diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..6cc231b898bef81ae56519a2ca23e1ffecd60405
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,40 @@
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.arrow filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.ckpt filter=lfs diff=lfs merge=lfs -text
+*.ftz filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.h5 filter=lfs diff=lfs merge=lfs -text
+*.joblib filter=lfs diff=lfs merge=lfs -text
+*.lfs.* filter=lfs diff=lfs merge=lfs -text
+*.mlmodel filter=lfs diff=lfs merge=lfs -text
+*.model filter=lfs diff=lfs merge=lfs -text
+*.msgpack filter=lfs diff=lfs merge=lfs -text
+*.npy filter=lfs diff=lfs merge=lfs -text
+*.npz filter=lfs diff=lfs merge=lfs -text
+*.onnx filter=lfs diff=lfs merge=lfs -text
+*.ot filter=lfs diff=lfs merge=lfs -text
+*.parquet filter=lfs diff=lfs merge=lfs -text
+*.pb filter=lfs diff=lfs merge=lfs -text
+*.pickle filter=lfs diff=lfs merge=lfs -text
+*.pkl filter=lfs diff=lfs merge=lfs -text
+*.pt filter=lfs diff=lfs merge=lfs -text
+*.pth filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.safetensors filter=lfs diff=lfs merge=lfs -text
+saved_model/**/* filter=lfs diff=lfs merge=lfs -text
+*.tar.* filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.tflite filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.wasm filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.zst filter=lfs diff=lfs merge=lfs -text
+*tfevents* filter=lfs diff=lfs merge=lfs -text
+*.stl filter=lfs diff=lfs merge=lfs -text
+*.lockb filter=lfs diff=lfs merge=lfs -text
+
+# Build output: store as plain binary (overrides LFS) so the wheel can ship real files
+dist/** -filter -diff -merge
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..4c4e572680dd7b3d4c7229a1a4a38a4f4c05b2b2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,48 @@
+# Dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# Testing
+/coverage
+
+# Production
+/build
+
+# Misc
+.DS_Store
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+.env*.local
+
+# Debug logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# TypeScript
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..2102ca142a0bdd91d73f49cd43666d6181713b39
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,35 @@
+FROM node:18-alpine
+
+# Use the existing node user (usually UID 1000)
+# Set up environment variables for the node user
+ENV HOME=/home/node \
+ PATH=/home/node/.local/bin:$PATH
+
+# Create and set up app directory owned by node user
+# Go to user's home directory first to ensure it exists
+WORKDIR $HOME
+RUN mkdir -p $HOME/app && \
+ chown -R node:node $HOME/app && \
+ chmod -R 755 $HOME/app # Set initial permissions
+WORKDIR $HOME/app
+
+# Switch to the node user
+USER node
+
+# Copy package files (owned by node)
+COPY --chown=node:node package*.json ./
+
+# Install dependencies
+RUN npm install
+
+# Copy the entire viewer directory (owned by node)
+COPY --chown=node:node . .
+
+# Build the application
+RUN npm run build
+
+# Expose port
+EXPOSE 7860
+
+# Start the application
+CMD ["npm", "run", "preview", "--", "--port", "7860", "--host"]
diff --git a/HTTPS_SETUP.md b/HTTPS_SETUP.md
new file mode 100644
index 0000000000000000000000000000000000000000..5e0cbd72e84308bc2258bd74f414c208e143e1e2
--- /dev/null
+++ b/HTTPS_SETUP.md
@@ -0,0 +1,219 @@
+# HTTPS Setup for Local Development
+
+This guide explains how to enable HTTPS for the frontend development server to ensure camera access works properly from mobile devices.
+
+## Why HTTPS is Required
+
+Modern browsers require HTTPS for accessing sensitive APIs like `getUserMedia()` (camera access), especially when:
+
+- Accessing from a different device (e.g., phone connecting to laptop's dev server)
+- The origin is not `localhost` or `127.0.0.1`
+
+Without HTTPS, camera access will be blocked by the browser's security policies.
+
+## Automatic Setup
+
+We've already set up HTTPS for you! The certificates are generated and the development server is configured to use them.
+
+### Quick Start
+
+1. **Start the development server:**
+
+ ```bash
+ npm run dev
+ ```
+
+2. **Access from your computer:**
+
+ - `https://localhost:8080`
+ - `https://127.0.0.1:8080`
+
+3. **Access from your phone:**
+ - `https://192.168.1.103:8080` (or your current local IP)
+
+## Manual Setup (if needed)
+
+If you need to regenerate certificates or set up on a new machine:
+
+```bash
+# Run our setup script
+./scripts/setup-https.sh
+```
+
+Or manually:
+
+```bash
+# Install mkcert
+brew install mkcert # macOS
+# For other platforms: https://github.com/FiloSottile/mkcert#installation
+
+# Install local CA
+mkcert -install
+
+# Generate certificates
+mkdir -p certs
+cd certs
+mkcert localhost 127.0.0.1 $(ipconfig getifaddr en0) ::1
+cd ..
+
+# Start development server
+npm run dev
+```
+
+## Certificate Details
+
+- **Location:** `./certs/`
+- **Files:**
+ - `localhost+3.pem` (certificate)
+ - `localhost+3-key.pem` (private key)
+- **Validity:** 3 months
+- **Domains:** localhost, 127.0.0.1, your local IP, IPv6 localhost
+
+## Mobile Device Setup
+
+### For Phone Access:
+
+1. **Ensure same WiFi network:** Your phone and development machine must be on the same WiFi network
+
+2. **Find your local IP:**
+
+ ```bash
+ ipconfig getifaddr en0 # macOS
+ # or check the console output when starting the dev server
+ ```
+
+3. **Access from phone:**
+
+ - Open Safari (iOS) or Chrome (Android)
+ - Navigate to `https://YOUR_LOCAL_IP:8080`
+ - You may see a security warning - this is normal for local certificates
+
+4. **Accept the certificate:**
+
+ - **iOS Safari:** Tap "Advanced" → "Proceed to website"
+ - **Android Chrome:** Tap "Advanced" → "Proceed to site (unsafe)"
+
+5. **Test camera access:**
+ - Go to the recording page OR use our camera test page: `https://YOUR_LOCAL_IP:8080/test-camera.html`
+ - Try to add a camera or click "Start Camera"
+ - The browser should prompt for camera permission
+ - Grant permission to test `getUserMedia()` functionality
+
+## Troubleshooting
+
+### Certificate Issues
+
+If you see certificate errors:
+
+```bash
+# Regenerate certificates
+rm -rf certs/
+./scripts/setup-https.sh
+```
+
+### IP Address Changes
+
+If your local IP changes (common with DHCP):
+
+```bash
+# Check current IP
+ipconfig getifaddr en0
+
+# Regenerate certificates with new IP
+rm -rf certs/
+./scripts/setup-https.sh
+```
+
+### Port Already in Use
+
+If port 8080 is busy:
+
+```bash
+# Check what's using the port
+lsof -ti:8080
+
+# Kill the process if needed
+kill -9 $(lsof -ti:8080)
+```
+
+### Browser Cache Issues
+
+If you're having issues after certificate changes:
+
+1. Clear browser cache and cookies for localhost
+2. Restart the browser
+3. Try incognito/private mode
+
+## Network Address Detection
+
+The app automatically detects the appropriate URL for phone access:
+
+- **Localhost access:** Automatically converts to network IP for QR codes
+- **Network access:** Uses current URL as-is
+- **Domain access:** Works with existing SSL certificates
+
+You can see the detected address in:
+
+- Browser console logs
+- Camera configuration UI (blue info box)
+
+## Security Notes
+
+- These certificates are only trusted on your local machine
+- They're perfect for development but should never be used in production
+- The private key is stored locally and should not be shared
+- Certificates expire after 3 months for security
+
+## Production Deployment
+
+For production:
+
+- Use a proper SSL certificate from a trusted CA
+- Configure your reverse proxy (nginx, Apache) or hosting platform
+- Ensure HTTPS is enforced across your entire application
+
+## Verification
+
+To verify HTTPS is working:
+
+1. **Check the URL bar:** Should show `https://` with a lock icon
+2. **Check console:** Network address detection should show HTTPS URLs
+3. **Test camera access:** `getUserMedia()` should work without security errors
+4. **Check from phone:** Camera permissions should be available
+
+### Quick Test Page
+
+We've included a dedicated camera test page for easy verification:
+
+**Desktop:** `https://localhost:8080/test-camera.html`
+**Phone:** `https://192.168.1.103:8080/test-camera.html`
+
+This page will:
+
+- ✅ Verify HTTPS and secure context
+- ✅ Test camera permissions and `getUserMedia()`
+- ✅ Display real-time video stream
+- ✅ Show detailed connection information
+- ✅ Provide troubleshooting guidance
+
+### Expected Results
+
+**✅ Success indicators:**
+
+- Green protocol check: "Camera access should work!"
+- Camera permission prompt appears
+- Video stream displays without errors
+- No console security warnings
+
+**❌ Failure indicators:**
+
+- Red protocol check: "Camera access may be blocked"
+- `NotAllowedError` or `NotSecureError` in console
+- No camera permission prompt
+- "This site is not secure" warnings
+
+## Additional Resources
+
+- [mkcert GitHub Repository](https://github.com/FiloSottile/mkcert)
+- [MDN: getUserMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
+- [Chrome Developer: HTTPS for Localhost](https://web.dev/how-to-use-local-https/)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..6243b501b4f98b07d866677de3fd7b5d5fd20624
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+---
+title: LeLab
+emoji: ⚡
+colorFrom: yellow
+colorTo: red
+sdk: docker
+app_port: 7860
+pinned: false
+short_description: Simple Interface to use LeRobot
+---
+
+
+
+# LeLab official Space repository
+
+If you've used [LeLab](https://huggingface.co/spaces/lerobot/LeLab) and want to contribute or found a bug, this is the place to be. This repo is directly hooked up to the Hugging Face space.
+
+Here's the equivalent [backend](https://github.com/huggingface/leLab) that keeps the FastAPI server you run to actually wrap the LeRobot library.
+
+We'll be updating this README shortly.
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/components.json b/components.json
new file mode 100644
index 0000000000000000000000000000000000000000..62e101166a31ade477811bd31f6be1ae44d45423
--- /dev/null
+++ b/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/index.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..e67846f70fbbe40407fc84875913595ab31c4a47
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,29 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ { ignores: ["dist"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "react-refresh/only-export-components": [
+ "warn",
+ { allowConstantExport: true },
+ ],
+ "@typescript-eslint/no-unused-vars": "off",
+ },
+ }
+);
diff --git a/index.html b/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..99bb4821df397183531ebde015006e4f65f1c3c2
--- /dev/null
+++ b/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+ LeLab
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..22c6ce4f2192e6d10bbede69a487bf281994c441
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,7724 @@
+{
+ "name": "vite_react_shadcn_ts",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "vite_react_shadcn_ts",
+ "version": "0.0.0",
+ "dependencies": {
+ "@hookform/resolvers": "^3.9.0",
+ "@radix-ui/react-accordion": "^1.2.0",
+ "@radix-ui/react-alert-dialog": "^1.1.1",
+ "@radix-ui/react-aspect-ratio": "^1.1.0",
+ "@radix-ui/react-avatar": "^1.1.0",
+ "@radix-ui/react-checkbox": "^1.1.1",
+ "@radix-ui/react-collapsible": "^1.1.0",
+ "@radix-ui/react-context-menu": "^2.2.1",
+ "@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
+ "@radix-ui/react-hover-card": "^1.1.1",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-menubar": "^1.1.1",
+ "@radix-ui/react-navigation-menu": "^1.2.0",
+ "@radix-ui/react-popover": "^1.1.1",
+ "@radix-ui/react-progress": "^1.1.0",
+ "@radix-ui/react-radio-group": "^1.2.0",
+ "@radix-ui/react-scroll-area": "^1.1.0",
+ "@radix-ui/react-select": "^2.1.1",
+ "@radix-ui/react-separator": "^1.1.0",
+ "@radix-ui/react-slider": "^1.2.0",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.1.0",
+ "@radix-ui/react-tabs": "^1.1.0",
+ "@radix-ui/react-toast": "^1.2.1",
+ "@radix-ui/react-toggle": "^1.1.0",
+ "@radix-ui/react-toggle-group": "^1.1.0",
+ "@radix-ui/react-tooltip": "^1.1.4",
+ "@react-three/drei": "^9.122.0",
+ "@react-three/fiber": "^8.18.0",
+ "@tanstack/react-query": "^5.56.2",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
+ "date-fns": "^3.6.0",
+ "embla-carousel-react": "^8.3.0",
+ "input-otp": "^1.2.4",
+ "jszip": "^3.10.1",
+ "lucide-react": "^0.462.0",
+ "next-themes": "^0.3.0",
+ "react": "^18.3.1",
+ "react-day-picker": "^8.10.1",
+ "react-dom": "^18.3.1",
+ "react-hook-form": "^7.53.0",
+ "react-resizable-panels": "^2.1.3",
+ "react-router-dom": "^6.26.2",
+ "recharts": "^2.12.7",
+ "sonner": "^1.5.0",
+ "tailwind-merge": "^2.5.2",
+ "tailwindcss-animate": "^1.0.7",
+ "three": "^0.177.0",
+ "urdf-loader": "^0.12.6",
+ "vaul": "^0.9.3",
+ "zod": "^3.23.8"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.9.0",
+ "@tailwindcss/typography": "^0.5.15",
+ "@types/node": "^22.5.5",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.9.0",
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+ "eslint-plugin-react-refresh": "^0.4.9",
+ "globals": "^15.9.0",
+ "lovable-tagger": "^1.1.7",
+ "postcss": "^8.4.47",
+ "tailwindcss": "^3.4.11",
+ "typescript": "^5.5.3",
+ "typescript-eslint": "^8.0.1",
+ "vite": "^5.4.1"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/parser": {
+ "version": "7.27.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
+ "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
+ "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
+ "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+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/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
+ "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
+ "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
+ "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
+ "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
+ "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
+ "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
+ "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
+ "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
+ "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
+ "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
+ "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
+ "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
+ "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
+ "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
+ "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
+ "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
+ "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
+ "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
+ "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
+ "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.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/config-array": {
+ "version": "0.20.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz",
+ "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz",
+ "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
+ "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.29.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz",
+ "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.2.tgz",
+ "integrity": "sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.15.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.0.tgz",
+ "integrity": "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz",
+ "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz",
+ "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.1",
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz",
+ "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "license": "MIT"
+ },
+ "node_modules/@hookform/resolvers": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
+ "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@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/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "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/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mediapipe/tasks-vision": {
+ "version": "0.10.17",
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
+ "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@monogrid/gainmap-js": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz",
+ "integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "promise-worker-transferable": "^1.0.4"
+ },
+ "peerDependencies": {
+ "three": ">= 0.159.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/@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.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz",
+ "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-collapsible": "1.1.11",
+ "@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-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-alert-dialog": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.14.tgz",
+ "integrity": "sha512-IOZfZ3nPvN6lXpJTBCunFQPRSvK8MDgSc1FB85xnIpUKOw9en0dJj8JmCAxV7BiZdtYlUpmrQjoTFkVYtdoWzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dialog": "1.1.14",
+ "@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-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-aspect-ratio": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz",
+ "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==",
+ "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-avatar": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
+ "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
+ "license": "MIT",
+ "dependencies": {
+ "@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-is-hydrated": "0.1.0",
+ "@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-checkbox": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
+ "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.4",
+ "@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-collapsible": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz",
+ "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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-presence": "1.1.4",
+ "@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"
+ },
+ "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-context-menu": {
+ "version": "2.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz",
+ "integrity": "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-menu": "2.1.15",
+ "@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-dialog": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
+ "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "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-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.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz",
+ "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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.15",
+ "@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.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "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-hover-card": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz",
+ "integrity": "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@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-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-label": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
+ "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
+ "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-menu": {
+ "version": "2.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz",
+ "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.10",
+ "@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-menubar": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.15.tgz",
+ "integrity": "sha512-Z71C7LGD+YDYo3TV81paUs8f3Zbmkvg6VLRQpKYfzioOE6n7fOhA3ApK/V/2Odolxjoc4ENk8AYCjohCNayd5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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-menu": "2.1.15",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.10",
+ "@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-navigation-menu": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.13.tgz",
+ "integrity": "sha512-WG8wWfDiJlSF5hELjwfjSGOXcBR/ZMhBFCGYe8vERpC39CQYZeq1PQ2kaYHdye3V95d06H89KGMsVCIE4LWo3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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.10",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.4",
+ "@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",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "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-popover": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
+ "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "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.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
+ "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
+ "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.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "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-progress": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
+ "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@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-radio-group": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz",
+ "integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.10",
+ "@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-roving-focus": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
+ "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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-scroll-area": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz",
+ "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.2",
+ "@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-presence": "1.1.4",
+ "@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"
+ },
+ "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-select": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
+ "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.2",
+ "@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.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@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-visually-hidden": "1.2.3",
+ "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-separator": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
+ "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
+ "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-slider": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz",
+ "integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.2",
+ "@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.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz",
+ "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz",
+ "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "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-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.10",
+ "@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-toast": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz",
+ "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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-dismissable-layer": "1.1.10",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@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",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "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-toggle": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.9.tgz",
+ "integrity": "sha512-ZoFkBBz9zv9GWer7wIjvdRxmh2wyc2oKWw6C6CseWd6/yq1DK/l5lJ+wnsmFwJZbBYqr02mrf8A2q/CVCuM3ZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@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-toggle-group": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.10.tgz",
+ "integrity": "sha512-kiU694Km3WFLTC75DdqgM/3Jauf3rD9wxeS9XtyWFKsBUeZA337lC+6uUazT7I1DhanZ5gyD5Stf8uf2dbQxOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "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-roving-focus": "1.1.10",
+ "@radix-ui/react-toggle": "1.1.9",
+ "@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-tooltip": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
+ "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "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-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-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "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/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "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/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/@react-spring/animated": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
+ "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/core": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz",
+ "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.5",
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-spring/donate"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/rafz": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz",
+ "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-spring/shared": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz",
+ "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/rafz": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@react-spring/three": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz",
+ "integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-spring/animated": "~9.7.5",
+ "@react-spring/core": "~9.7.5",
+ "@react-spring/shared": "~9.7.5",
+ "@react-spring/types": "~9.7.5"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": ">=6.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "three": ">=0.126"
+ }
+ },
+ "node_modules/@react-spring/types": {
+ "version": "9.7.5",
+ "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz",
+ "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
+ "license": "MIT"
+ },
+ "node_modules/@react-three/drei": {
+ "version": "9.122.0",
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz",
+ "integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mediapipe/tasks-vision": "0.10.17",
+ "@monogrid/gainmap-js": "^3.0.6",
+ "@react-spring/three": "~9.7.5",
+ "@use-gesture/react": "^10.3.1",
+ "camera-controls": "^2.9.0",
+ "cross-env": "^7.0.3",
+ "detect-gpu": "^5.0.56",
+ "glsl-noise": "^0.0.0",
+ "hls.js": "^1.5.17",
+ "maath": "^0.10.8",
+ "meshline": "^3.3.1",
+ "react-composer": "^5.0.3",
+ "stats-gl": "^2.2.8",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.1.3",
+ "three-mesh-bvh": "^0.7.8",
+ "three-stdlib": "^2.35.6",
+ "troika-three-text": "^0.52.0",
+ "tunnel-rat": "^0.1.2",
+ "utility-types": "^3.11.0",
+ "zustand": "^5.0.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": "^8",
+ "react": "^18",
+ "react-dom": "^18",
+ "three": ">=0.137"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
+ "integrity": "sha512-FYZZqD0UUHUswKz3LQl2Z7H24AhD14XGTsIRw3SJaXUxyfVMi+1yiZGmqTcPt/CkPpdU7rrxqcyQ1zJE5DjvIQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/react-reconciler": "^0.26.7",
+ "@types/webxr": "*",
+ "base64-js": "^1.5.1",
+ "buffer": "^6.0.3",
+ "its-fine": "^1.0.6",
+ "react-reconciler": "^0.27.0",
+ "react-use-measure": "^2.1.7",
+ "scheduler": "^0.21.0",
+ "suspend-react": "^0.1.3",
+ "zustand": "^3.7.1"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-file-system": ">=11.0",
+ "expo-gl": ">=11.0",
+ "react": ">=18 <19",
+ "react-dom": ">=18 <19",
+ "react-native": ">=0.64",
+ "three": ">=0.133"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-file-system": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber/node_modules/zustand": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz",
+ "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
+ "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.11",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz",
+ "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz",
+ "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz",
+ "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz",
+ "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz",
+ "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz",
+ "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz",
+ "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz",
+ "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz",
+ "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz",
+ "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz",
+ "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz",
+ "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz",
+ "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz",
+ "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz",
+ "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz",
+ "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz",
+ "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz",
+ "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz",
+ "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz",
+ "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz",
+ "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/core": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.1.tgz",
+ "integrity": "sha512-aKXdDTqxTVFl/bKQZ3EQUjEMBEoF6JBv29moMZq0kbVO43na6u/u+3Vcbhbrh+A2N0X5OL4RaveuWfAjEgOmeA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.23"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.12.1",
+ "@swc/core-darwin-x64": "1.12.1",
+ "@swc/core-linux-arm-gnueabihf": "1.12.1",
+ "@swc/core-linux-arm64-gnu": "1.12.1",
+ "@swc/core-linux-arm64-musl": "1.12.1",
+ "@swc/core-linux-x64-gnu": "1.12.1",
+ "@swc/core-linux-x64-musl": "1.12.1",
+ "@swc/core-win32-arm64-msvc": "1.12.1",
+ "@swc/core-win32-ia32-msvc": "1.12.1",
+ "@swc/core-win32-x64-msvc": "1.12.1"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.1.tgz",
+ "integrity": "sha512-nUjWVcJ3YS2N40ZbKwYO2RJ4+o2tWYRzNOcIQp05FqW0+aoUCVMdAUUzQinPDynfgwVshDAXCKemY8X7nN5MaA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.1.tgz",
+ "integrity": "sha512-OGm4a4d3OeJn+tRt8H/eiHgTFrJbS6r8mi/Ob65tAEXZGHN900T2kR7c5ALr0V2hBOQ8BfhexwPoQlGQP/B95w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.1.tgz",
+ "integrity": "sha512-76YeeQKyK0EtNkQiNBZ0nbVGooPf9IucY0WqVXVpaU4wuG7ZyLEE2ZAIgXafIuzODGQoLfetue7I8boMxh1/MA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.1.tgz",
+ "integrity": "sha512-BxJDIJPq1+aCh9UsaSAN6wo3tuln8UhNXruOrzTI8/ElIig/3sAueDM6Eq7GvZSGGSA7ljhNATMJ0elD7lFatQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.1.tgz",
+ "integrity": "sha512-NhLdbffSXvY0/FwUSAl4hKBlpe5GHQGXK8DxTo3HHjLsD9sCPYieo3vG0NQoUYAy4ZUY1WeGjyxeq4qZddJzEQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.1.tgz",
+ "integrity": "sha512-CrYnV8SZIgArQ9LKH0xEF95PKXzX9WkRSc5j55arOSBeDCeDUQk1Bg/iKdnDiuj5HC1hZpvzwMzSBJjv+Z70jA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.1.tgz",
+ "integrity": "sha512-BQMl3d0HaGB0/h2xcKlGtjk/cGRn2tnbsaChAKcjFdCepblKBCz1pgO/mL7w5iXq3s57wMDUn++71/a5RAkZOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.1.tgz",
+ "integrity": "sha512-b7NeGnpqTfmIGtUqXBl0KqoSmOnH64nRZoT5l4BAGdvwY7nxitWR94CqZuwyLPty/bLywmyDA9uO12Kvgb3+gg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.1.tgz",
+ "integrity": "sha512-iU/29X2D7cHBp1to62cUg/5Xk8K+lyOJiKIGGW5rdzTW/c2zz3d/ehgpzVP/rqC4NVr88MXspqHU4il5gmDajw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.1.tgz",
+ "integrity": "sha512-+Zh+JKDwiFqV5N9yAd2DhYVGPORGh9cfenu1ptr9yge+eHAf7vZJcC3rnj6QMR1QJh0Y5VC9+YBjRFjZVA7XDw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.23",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz",
+ "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
+ "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash.castarray": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2",
+ "postcss-selector-parser": "6.0.10"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.80.7",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.7.tgz",
+ "integrity": "sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.80.7",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.7.tgz",
+ "integrity": "sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.80.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/draco3d": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
+ "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/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.15.31",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz",
+ "integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.3",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
+ "license": "MIT"
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.23",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
+ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
+ "license": "MIT",
+ "peer": true,
+ "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",
+ "peer": true,
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/react-reconciler": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.26.7.tgz",
+ "integrity": "sha512-mBDYl8x+oyPX/VBb3E638N0B7xG+SPk/EAMcVPeexqus/5aTpTphQi0curhhshOqRrc9t6OPoJfEUkbymse/lQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.177.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.177.0.tgz",
+ "integrity": "sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": "*",
+ "@webgpu/types": "*",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~0.18.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.22",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.22.tgz",
+ "integrity": "sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz",
+ "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.34.0",
+ "@typescript-eslint/type-utils": "8.34.0",
+ "@typescript-eslint/utils": "8.34.0",
+ "@typescript-eslint/visitor-keys": "8.34.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.34.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz",
+ "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.34.0",
+ "@typescript-eslint/types": "8.34.0",
+ "@typescript-eslint/typescript-estree": "8.34.0",
+ "@typescript-eslint/visitor-keys": "8.34.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz",
+ "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.34.0",
+ "@typescript-eslint/types": "^8.34.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz",
+ "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.34.0",
+ "@typescript-eslint/visitor-keys": "8.34.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz",
+ "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz",
+ "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "8.34.0",
+ "@typescript-eslint/utils": "8.34.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz",
+ "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz",
+ "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.34.0",
+ "@typescript-eslint/tsconfig-utils": "8.34.0",
+ "@typescript-eslint/types": "8.34.0",
+ "@typescript-eslint/visitor-keys": "8.34.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz",
+ "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.34.0",
+ "@typescript-eslint/types": "8.34.0",
+ "@typescript-eslint/typescript-estree": "8.34.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz",
+ "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.34.0",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@use-gesture/core": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
+ "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/react": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
+ "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@use-gesture/core": "10.3.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react-swc": {
+ "version": "3.10.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz",
+ "integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.11",
+ "@swc/core": "^1.11.31"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0"
+ }
+ },
+ "node_modules/@webgpu/types": {
+ "version": "0.1.61",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.61.tgz",
+ "integrity": "sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ==",
+ "license": "BSD-3-Clause"
+ },
+ "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",
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.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/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "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/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/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==",
+ "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/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "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/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/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.25.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
+ "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
+ "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",
+ "peer": true,
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001718",
+ "electron-to-chromium": "^1.5.160",
+ "node-releases": "^2.0.19",
+ "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": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "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.2.1"
+ }
+ },
+ "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/camera-controls": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.1.tgz",
+ "integrity": "sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001723",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
+ "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
+ "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/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/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/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "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/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/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/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
+ "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-gpu": {
+ "version": "5.0.70",
+ "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
+ "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
+ "license": "MIT",
+ "dependencies": {
+ "webgl-constants": "^1.1.1"
+ }
+ },
+ "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/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "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/draco3d": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
+ "license": "Apache-2.0"
+ },
+ "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/electron-to-chromium": {
+ "version": "1.5.167",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz",
+ "integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/embla-carousel": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
+ "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/embla-carousel-react": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz",
+ "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==",
+ "license": "MIT",
+ "dependencies": {
+ "embla-carousel": "8.6.0",
+ "embla-carousel-reactive-utils": "8.6.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/embla-carousel-reactive-utils": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz",
+ "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.6.0"
+ }
+ },
+ "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/esbuild": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
+ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.5",
+ "@esbuild/android-arm": "0.25.5",
+ "@esbuild/android-arm64": "0.25.5",
+ "@esbuild/android-x64": "0.25.5",
+ "@esbuild/darwin-arm64": "0.25.5",
+ "@esbuild/darwin-x64": "0.25.5",
+ "@esbuild/freebsd-arm64": "0.25.5",
+ "@esbuild/freebsd-x64": "0.25.5",
+ "@esbuild/linux-arm": "0.25.5",
+ "@esbuild/linux-arm64": "0.25.5",
+ "@esbuild/linux-ia32": "0.25.5",
+ "@esbuild/linux-loong64": "0.25.5",
+ "@esbuild/linux-mips64el": "0.25.5",
+ "@esbuild/linux-ppc64": "0.25.5",
+ "@esbuild/linux-riscv64": "0.25.5",
+ "@esbuild/linux-s390x": "0.25.5",
+ "@esbuild/linux-x64": "0.25.5",
+ "@esbuild/netbsd-arm64": "0.25.5",
+ "@esbuild/netbsd-x64": "0.25.5",
+ "@esbuild/openbsd-arm64": "0.25.5",
+ "@esbuild/openbsd-x64": "0.25.5",
+ "@esbuild/sunos-x64": "0.25.5",
+ "@esbuild/win32-arm64": "0.25.5",
+ "@esbuild/win32-ia32": "0.25.5",
+ "@esbuild/win32-x64": "0.25.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": "9.29.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
+ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.20.1",
+ "@eslint/config-helpers": "^0.2.1",
+ "@eslint/core": "^0.14.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.29.0",
+ "@eslint/plugin-kit": "^0.3.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "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 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.20",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz",
+ "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.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/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/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": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "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-equals": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
+ "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "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/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.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/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/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/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/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/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": "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/glob/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/globals": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glsl-noise": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
+ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
+ "license": "MIT"
+ },
+ "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/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/hls.js": {
+ "version": "1.6.5",
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.5.tgz",
+ "integrity": "sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ==",
+ "license": "Apache-2.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==",
+ "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/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
+ "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/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.4.2",
+ "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
+ "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
+ "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/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "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-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-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-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+ "license": "MIT"
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "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/its-fine": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
+ "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0"
+ }
+ },
+ "node_modules/its-fine/node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "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/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-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/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/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.castarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
+ "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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"
+ },
+ "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/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/lovable-tagger": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/lovable-tagger/-/lovable-tagger-1.1.8.tgz",
+ "integrity": "sha512-0a8lEpsgk+Z6crd+HE+GTmVzB0f++lMyTgW3QUF+hYZBjsA384RqUfCZAYccHlMLxYPI+dk7zjVuIvJcq8PiBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.9",
+ "@babel/types": "^7.25.8",
+ "esbuild": "^0.25.0",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.12",
+ "tailwindcss": "^3.4.17"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0"
+ }
+ },
+ "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/lucide-react": {
+ "version": "0.462.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.462.0.tgz",
+ "integrity": "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/maath": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
+ "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.134.0",
+ "three": ">=0.134.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/meshline": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
+ "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.137"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "0.18.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
+ "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
+ "license": "MIT"
+ },
+ "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/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/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/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/next-themes": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
+ "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18",
+ "react-dom": "^16.8 || ^17 || ^18"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/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/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/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/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
+ "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/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "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/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/postcss": {
+ "version": "8.5.5",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz",
+ "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==",
+ "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",
+ "peer": true,
+ "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.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "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-nested/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-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/potpack": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
+ "license": "ISC"
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/promise-worker-transferable": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
+ "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "is-promise": "^2.1.0",
+ "lie": "^3.0.2"
+ }
+ },
+ "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/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "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",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-composer": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz",
+ "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.6.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-day-picker": {
+ "version": "8.10.1",
+ "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
+ "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==",
+ "license": "MIT",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/gpbl"
+ },
+ "peerDependencies": {
+ "date-fns": "^2.28.0 || ^3.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.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",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-dom/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/react-hook-form": {
+ "version": "7.57.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
+ "integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
+ "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-reconciler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.27.0.tgz",
+ "integrity": "sha512-HmMDKciQjYmBRGuuhIaKA1ba/7a+UsM5FzOZsMO2JYHt9Jh8reCb7j1eDC95NOyUlKM9KRyvdx0flBuDvYSBoA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.21.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.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-resizable-panels": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz",
+ "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==",
+ "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-router": {
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
+ "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
+ "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.0",
+ "react-router": "6.30.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/react-smooth": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
+ "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-equals": "^5.0.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "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/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/react-use-measure": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.13",
+ "react-dom": ">=16.13"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "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/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "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/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/recharts": {
+ "version": "2.15.3",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.3.tgz",
+ "integrity": "sha512-EdOPzTwcFSuqtvkDoaM5ws/Km1+WTAO2eizL7rqiG0V2UVhTnz0m7J2i0CjVPUCdEkZImaWvXLbZDS2H5t6GFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.0.0",
+ "eventemitter3": "^4.0.1",
+ "lodash": "^4.17.21",
+ "react-is": "^18.3.1",
+ "react-smooth": "^4.0.4",
+ "recharts-scale": "^0.4.4",
+ "tiny-invariant": "^1.3.1",
+ "victory-vendor": "^36.6.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/recharts-scale": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
+ "license": "MIT",
+ "dependencies": {
+ "decimal.js-light": "^2.4.1"
+ }
+ },
+ "node_modules/recharts/node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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-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/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/rollup": {
+ "version": "4.43.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
+ "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.7"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.43.0",
+ "@rollup/rollup-android-arm64": "4.43.0",
+ "@rollup/rollup-darwin-arm64": "4.43.0",
+ "@rollup/rollup-darwin-x64": "4.43.0",
+ "@rollup/rollup-freebsd-arm64": "4.43.0",
+ "@rollup/rollup-freebsd-x64": "4.43.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.43.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.43.0",
+ "@rollup/rollup-linux-arm64-musl": "4.43.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.43.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.43.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.43.0",
+ "@rollup/rollup-linux-x64-gnu": "4.43.0",
+ "@rollup/rollup-linux-x64-musl": "4.43.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.43.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.43.0",
+ "@rollup/rollup-win32-x64-msvc": "4.43.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup/node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
+ "integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
+ "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/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/sonner": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
+ "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
+ "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-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/stats-gl": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
+ "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/three": "*",
+ "three": "^0.170.0"
+ },
+ "peerDependencies": {
+ "@types/three": "*",
+ "three": "*"
+ }
+ },
+ "node_modules/stats-gl/node_modules/three": {
+ "version": "0.170.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
+ "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
+ "license": "MIT"
+ },
+ "node_modules/stats.js": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
+ "license": "MIT"
+ },
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "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/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/string-width-cjs/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/string-width-cjs/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/string-width-cjs/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": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "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-ansi-cjs/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/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/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/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
+ "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "license": "MIT",
+ "peer": true,
+ "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.6",
+ "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",
+ "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/tailwindcss/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/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/three": {
+ "version": "0.177.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.177.0.tgz",
+ "integrity": "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/three-mesh-bvh": {
+ "version": "0.7.8",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz",
+ "integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==",
+ "deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.151.0"
+ }
+ },
+ "node_modules/three-stdlib": {
+ "version": "2.36.0",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.0.tgz",
+ "integrity": "sha512-kv0Byb++AXztEGsULgMAs8U2jgUdz6HPpAB/wDJnLiLlaWQX2APHhiTJIN7rqW+Of0eRgcp7jn05U1BsCP3xBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/draco3d": "^1.4.0",
+ "@types/offscreencanvas": "^2019.6.4",
+ "@types/webxr": "^0.5.2",
+ "draco3d": "^1.4.1",
+ "fflate": "^0.6.9",
+ "potpack": "^1.0.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.128.0"
+ }
+ },
+ "node_modules/three-stdlib/node_modules/fflate": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
+ "license": "MIT"
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
+ "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/troika-three-text": {
+ "version": "0.52.4",
+ "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
+ "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.0.2",
+ "troika-three-utils": "^0.52.4",
+ "troika-worker-utils": "^0.52.0",
+ "webgl-sdf-generator": "1.1.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-three-utils": {
+ "version": "0.52.4",
+ "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
+ "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-worker-utils": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
+ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
+ "license": "MIT"
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "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/tunnel-rat": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
+ "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "zustand": "^4.3.2"
+ }
+ },
+ "node_modules/tunnel-rat/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.34.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz",
+ "integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.34.0",
+ "@typescript-eslint/parser": "8.34.0",
+ "@typescript-eslint/utils": "8.34.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.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/urdf-loader": {
+ "version": "0.12.6",
+ "resolved": "https://registry.npmjs.org/urdf-loader/-/urdf-loader-0.12.6.tgz",
+ "integrity": "sha512-EwpgOCPe6Tep2+MXoo/r13keHaKQXMcM+4s9+jX0NRxNS/QSNuP5JPdk5AIgWEoEB43AkEj9Vk+Nr53NkXgSbA==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "three": ">=0.152.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-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/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/utility-types": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/vaul": {
+ "version": "0.9.9",
+ "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
+ "integrity": "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "36.9.2",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.19",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
+ "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "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_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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/webgl-constants": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+ "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
+ },
+ "node_modules/webgl-sdf-generator": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
+ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
+ "license": "MIT"
+ },
+ "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/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": "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/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/yaml": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
+ "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.64",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz",
+ "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz",
+ "integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..d8b5178b4b101a99c3ae4fc58f8e090bf18f6940
--- /dev/null
+++ b/package.json
@@ -0,0 +1,88 @@
+{
+ "name": "vite_react_shadcn_ts",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "build:dev": "vite build --mode development",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^3.9.0",
+ "@radix-ui/react-accordion": "^1.2.0",
+ "@radix-ui/react-alert-dialog": "^1.1.1",
+ "@radix-ui/react-aspect-ratio": "^1.1.0",
+ "@radix-ui/react-avatar": "^1.1.0",
+ "@radix-ui/react-checkbox": "^1.1.1",
+ "@radix-ui/react-collapsible": "^1.1.0",
+ "@radix-ui/react-context-menu": "^2.2.1",
+ "@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-dropdown-menu": "^2.1.1",
+ "@radix-ui/react-hover-card": "^1.1.1",
+ "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-menubar": "^1.1.1",
+ "@radix-ui/react-navigation-menu": "^1.2.0",
+ "@radix-ui/react-popover": "^1.1.1",
+ "@radix-ui/react-progress": "^1.1.0",
+ "@radix-ui/react-radio-group": "^1.2.0",
+ "@radix-ui/react-scroll-area": "^1.1.0",
+ "@radix-ui/react-select": "^2.1.1",
+ "@radix-ui/react-separator": "^1.1.0",
+ "@radix-ui/react-slider": "^1.2.0",
+ "@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.1.0",
+ "@radix-ui/react-tabs": "^1.1.0",
+ "@radix-ui/react-toast": "^1.2.1",
+ "@radix-ui/react-toggle": "^1.1.0",
+ "@radix-ui/react-toggle-group": "^1.1.0",
+ "@radix-ui/react-tooltip": "^1.1.4",
+ "@react-three/drei": "^9.122.0",
+ "@react-three/fiber": "^8.18.0",
+ "@tanstack/react-query": "^5.56.2",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
+ "date-fns": "^3.6.0",
+ "embla-carousel-react": "^8.3.0",
+ "input-otp": "^1.2.4",
+ "jszip": "^3.10.1",
+ "lucide-react": "^0.462.0",
+ "next-themes": "^0.3.0",
+ "react": "^18.3.1",
+ "react-day-picker": "^8.10.1",
+ "react-dom": "^18.3.1",
+ "react-hook-form": "^7.53.0",
+ "react-resizable-panels": "^2.1.3",
+ "react-router-dom": "^6.26.2",
+ "recharts": "^2.12.7",
+ "sonner": "^1.5.0",
+ "tailwind-merge": "^2.5.2",
+ "tailwindcss-animate": "^1.0.7",
+ "three": "^0.177.0",
+ "urdf-loader": "^0.12.6",
+ "vaul": "^0.9.3",
+ "zod": "^3.23.8"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.9.0",
+ "@tailwindcss/typography": "^0.5.15",
+ "@types/node": "^22.5.5",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.9.0",
+ "eslint-plugin-react-hooks": "^5.1.0-rc.0",
+ "eslint-plugin-react-refresh": "^0.4.9",
+ "globals": "^15.9.0",
+ "lovable-tagger": "^1.1.7",
+ "postcss": "^8.4.47",
+ "tailwindcss": "^3.4.11",
+ "typescript": "^5.5.3",
+ "typescript-eslint": "^8.0.1",
+ "vite": "^5.4.1"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e7af2b7f1a6f391da1631d93968a9d487ba977d
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..424101de717bf411d1db8456030c97d184d4c005
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png b/public/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png
new file mode 100644
index 0000000000000000000000000000000000000000..39595804e566c6c8298b4dfbb31bf7f314c2e562
Binary files /dev/null and b/public/lovable-uploads/5e648747-34b7-4d8f-93fd-4dbd00aeeefc.png differ
diff --git a/public/placeholder.svg b/public/placeholder.svg
new file mode 100644
index 0000000000000000000000000000000000000000..9b13eb69f97b905ae787cf4493758dd6e7bc2e72
--- /dev/null
+++ b/public/placeholder.svg
@@ -0,0 +1 @@
+
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000000000000000000000000000000000000..6018e701fc7dd0317cda9eceea390524322e8a05
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,14 @@
+User-agent: Googlebot
+Allow: /
+
+User-agent: Bingbot
+Allow: /
+
+User-agent: Twitterbot
+Allow: /
+
+User-agent: facebookexternalhit
+Allow: /
+
+User-agent: *
+Allow: /
diff --git a/public/so-101-urdf/CMakeLists.txt b/public/so-101-urdf/CMakeLists.txt
new file mode 100755
index 0000000000000000000000000000000000000000..775381afc316a8145c5ac319e3ffc610262779de
--- /dev/null
+++ b/public/so-101-urdf/CMakeLists.txt
@@ -0,0 +1,37 @@
+cmake_minimum_required(VERSION 3.10.2)
+
+project(so_arm_description)
+
+find_package(ament_cmake REQUIRED)
+find_package(urdf REQUIRED)
+
+# Install the mesh files from SO101/assets
+install(
+ DIRECTORY
+ SO101/assets/
+ DESTINATION
+ share/${PROJECT_NAME}/meshes
+ FILES_MATCHING PATTERN "*.stl"
+)
+
+# Install URDF files
+install(
+ DIRECTORY
+ urdf/
+ DESTINATION
+ share/${PROJECT_NAME}/urdf
+ FILES_MATCHING PATTERN "*.urdf"
+)
+
+# Install other directories
+install(
+ DIRECTORY
+ meshes
+ config
+ launch
+ DESTINATION
+ share/${PROJECT_NAME}
+ OPTIONAL
+)
+
+ament_package()
diff --git a/public/so-101-urdf/README.md b/public/so-101-urdf/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b17a93230eaeec5830e207008af3a4f8517b218e
--- /dev/null
+++ b/public/so-101-urdf/README.md
@@ -0,0 +1,45 @@
+# SO-ARM ROS2 URDF Package
+
+A complete ROS2 package for the SO-ARM101 robotic arm with URDF description.
+
+## 📋 Overview
+
+This package provides a complete ROS2 implementation for the SO-ARM101 robotic arm, including:
+
+- URDF robot description with visual and collision meshes
+- RViz visualization with pre-configured displays
+- Launch files for easy robot visualization
+- Integration with MoveIt for motion planning
+- Joint state publishers for interactive control
+
+## 🎯 Original Source
+
+https://github.com/TheRobotStudio/SO-ARM100/tree/main/Simulation/SO101
+
+## 🚀 Key Improvements Made
+
+### 1. **Complete ROS2 Package Structure**
+
+- ✅ Proper `package.xml` with all necessary dependencies
+- ✅ CMakeLists.txt for ROS2 build system
+- ✅ Organized directory structure following ROS2 conventions
+
+### 2. **Enhanced Visualization**
+
+- ✅ Fixed mesh file paths for proper package integration
+
+### Build Instructions
+
+1. Clone this repository into your ROS2 workspace:
+
+ ```bash
+ cd ~/your_ros2_ws/src
+ git clone so_arm_description
+ ```
+
+2. Build the package:
+ ```bash
+ cd ~/your_ros2_ws
+ colcon build --packages-select so_arm_description
+ source install/setup.bash
+ ```
diff --git a/public/so-101-urdf/config/joint_names_so_arm_urdf.yaml b/public/so-101-urdf/config/joint_names_so_arm_urdf.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1f0c831755649001a8ba1a88e81e162b944a710c
--- /dev/null
+++ b/public/so-101-urdf/config/joint_names_so_arm_urdf.yaml
@@ -0,0 +1 @@
+controller_joint_names: ['', 'Rotation', 'Pitch', 'Elbow', 'Wrist_Pitch', 'Wrist_Roll', 'Jaw', ]
diff --git a/public/so-101-urdf/joints_properties.xml b/public/so-101-urdf/joints_properties.xml
new file mode 100644
index 0000000000000000000000000000000000000000..684ffd45766219a22be6e7dc23bcfd3c2f8c9191
--- /dev/null
+++ b/public/so-101-urdf/joints_properties.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/so-101-urdf/meshes/base_motor_holder_so101_v1.stl b/public/so-101-urdf/meshes/base_motor_holder_so101_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..ac9c38076fe1036517faf0bccadea5de9dce0097
--- /dev/null
+++ b/public/so-101-urdf/meshes/base_motor_holder_so101_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8cd2f241037ea377af1191fffe0dd9d9006beea6dcc48543660ed41647072424
+size 1877084
diff --git a/public/so-101-urdf/meshes/base_so101_v2.stl b/public/so-101-urdf/meshes/base_so101_v2.stl
new file mode 100644
index 0000000000000000000000000000000000000000..503d30be06a91e401ba8d46ebb7e650866229550
--- /dev/null
+++ b/public/so-101-urdf/meshes/base_so101_v2.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bb12b7026575e1f70ccc7240051f9d943553bf34e5128537de6cd86fae33924d
+size 471584
diff --git a/public/so-101-urdf/meshes/motor_holder_so101_base_v1.stl b/public/so-101-urdf/meshes/motor_holder_so101_base_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..f8e3d75c027f28bb672f830ec6e0795567c1b7c9
--- /dev/null
+++ b/public/so-101-urdf/meshes/motor_holder_so101_base_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:31242ae6fb59d8b15c66617b88ad8e9bded62d57c35d11c0c43a70d2f4caa95b
+size 1129384
diff --git a/public/so-101-urdf/meshes/motor_holder_so101_wrist_v1.stl b/public/so-101-urdf/meshes/motor_holder_so101_wrist_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..e55b7194683c6ac301504c6f59137362f0ebd13e
--- /dev/null
+++ b/public/so-101-urdf/meshes/motor_holder_so101_wrist_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:887f92e6013cb64ea3a1ab8675e92da1e0beacfd5e001f972523540545e08011
+size 1052184
diff --git a/public/so-101-urdf/meshes/moving_jaw_so101_v1.stl b/public/so-101-urdf/meshes/moving_jaw_so101_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..eb17d253df8a84a88472ecc7f859d3b8b4d78884
--- /dev/null
+++ b/public/so-101-urdf/meshes/moving_jaw_so101_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:785a9dded2f474bc1d869e0d3dae398a3dcd9c0c345640040472210d2861fa9d
+size 1413584
diff --git a/public/so-101-urdf/meshes/rotation_pitch_so101_v1.stl b/public/so-101-urdf/meshes/rotation_pitch_so101_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..b536cb4100c1f204f8a9d9b182acdc4a3afbc66c
--- /dev/null
+++ b/public/so-101-urdf/meshes/rotation_pitch_so101_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9be900cc2a2bf718102841ef82ef8d2873842427648092c8ed2ca1e2ef4ffa34
+size 883684
diff --git a/public/so-101-urdf/meshes/sts3215_03a_no_horn_v1.stl b/public/so-101-urdf/meshes/sts3215_03a_no_horn_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..18e9335673f6d46ea8fd0a03a791516203eb6f4c
--- /dev/null
+++ b/public/so-101-urdf/meshes/sts3215_03a_no_horn_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:75ef3781b752e4065891aea855e34dc161a38a549549cd0970cedd07eae6f887
+size 865884
diff --git a/public/so-101-urdf/meshes/sts3215_03a_v1.stl b/public/so-101-urdf/meshes/sts3215_03a_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..a14c57b9033b82f1daa38f45e7e3c91343702df4
--- /dev/null
+++ b/public/so-101-urdf/meshes/sts3215_03a_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a37c871fb502483ab96c256baf457d36f2e97afc9205313d9c5ab275ef941cd0
+size 954084
diff --git a/public/so-101-urdf/meshes/under_arm_so101_v1.stl b/public/so-101-urdf/meshes/under_arm_so101_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..47b611ef939e452f791ae749756f717317922cfd
--- /dev/null
+++ b/public/so-101-urdf/meshes/under_arm_so101_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d01d1f2de365651dcad9d6669e94ff87ff7652b5bb2d10752a66a456a86dbc71
+size 1975884
diff --git a/public/so-101-urdf/meshes/upper_arm_so101_v1.stl b/public/so-101-urdf/meshes/upper_arm_so101_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..8832740f9540065e6006907a9a826b01f96cd122
--- /dev/null
+++ b/public/so-101-urdf/meshes/upper_arm_so101_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:475056e03a17e71919b82fd88ab9a0b898ab50164f2a7943652a6b2941bb2d4f
+size 1303484
diff --git a/public/so-101-urdf/meshes/waveshare_mounting_plate_so101_v2.stl b/public/so-101-urdf/meshes/waveshare_mounting_plate_so101_v2.stl
new file mode 100644
index 0000000000000000000000000000000000000000..e0d90d5b6554ab8ca91928fa6521a675089beb12
--- /dev/null
+++ b/public/so-101-urdf/meshes/waveshare_mounting_plate_so101_v2.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e197e24005a07d01bbc06a8c42311664eaeda415bf859f68fa247884d0f1a6e9
+size 62784
diff --git a/public/so-101-urdf/meshes/wrist_roll_follower_so101_v1.stl b/public/so-101-urdf/meshes/wrist_roll_follower_so101_v1.stl
new file mode 100644
index 0000000000000000000000000000000000000000..9a5fa8fe2d7d8e59cd4a30d4dba0ca337513ab4a
--- /dev/null
+++ b/public/so-101-urdf/meshes/wrist_roll_follower_so101_v1.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4b17b410a12d64ec39554abc3e8054d8a97384b2dc4a8d95a5ecb2a93670f5f4
+size 1439884
diff --git a/public/so-101-urdf/meshes/wrist_roll_pitch_so101_v2.stl b/public/so-101-urdf/meshes/wrist_roll_pitch_so101_v2.stl
new file mode 100644
index 0000000000000000000000000000000000000000..2f531712f88ec01d09824ee8e27c791a4616516f
--- /dev/null
+++ b/public/so-101-urdf/meshes/wrist_roll_pitch_so101_v2.stl
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6c7ec5525b4d8b9e397a30ab4bb0037156a5d5f38a4adf2c7d943d6c56eda5ae
+size 2699784
diff --git a/public/so-101-urdf/package.xml b/public/so-101-urdf/package.xml
new file mode 100644
index 0000000000000000000000000000000000000000..91eef666560a8641edec92221b2142a20c227bde
--- /dev/null
+++ b/public/so-101-urdf/package.xml
@@ -0,0 +1,26 @@
+
+
+
+ so_arm_description
+ 1.0.0
+ SO-ARM101 URDF Resources
+
+ LycheeAI
+
+ LycheeAI
+
+ BSD
+
+ ament_cmake
+
+ urdf
+ robot_state_publisher
+ joint_state_publisher
+ joint_state_publisher_gui
+ rviz2
+ xacro
+
+
+ ament_cmake
+
+
diff --git a/public/so-101-urdf/urdf/so101_new_calib.urdf b/public/so-101-urdf/urdf/so101_new_calib.urdf
new file mode 100644
index 0000000000000000000000000000000000000000..8967a31f41574f137d02d4063fd18537fbb6970d
--- /dev/null
+++ b/public/so-101-urdf/urdf/so101_new_calib.urdf
@@ -0,0 +1,435 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ hardware_interface/PositionJointInterface
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ hardware_interface/PositionJointInterface
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ hardware_interface/PositionJointInterface
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ hardware_interface/PositionJointInterface
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ hardware_interface/PositionJointInterface
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ transmission_interface/SimpleTransmission
+
+ hardware_interface/PositionJointInterface
+
+
+ hardware_interface/PositionJointInterface
+ 1
+
+
+
+
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c2658d7d1b31848c3b71960543cb0368e56cd4c7
--- /dev/null
+++ b/src/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b85c50f2e292b10b64c622cfd5adcfaec31bfbcb
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,61 @@
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ThemeProvider } from "@/contexts/ThemeContext";
+import { UrdfProvider } from "@/contexts/UrdfContext";
+import { DragAndDropProvider } from "@/contexts/DragAndDropContext";
+import { Toaster } from "@/components/ui/toaster";
+import Landing from "@/pages/Landing";
+import Teleoperation from "@/pages/Teleoperation";
+import Calibration from "@/pages/Calibration";
+import Recording from "@/pages/Recording";
+import Training from "@/pages/Training";
+import Inference from "@/pages/Inference";
+import EditDataset from "@/pages/EditDataset";
+import Upload from "@/pages/Upload";
+
+import NotFound from "@/pages/NotFound";
+import SingleTabGuard from "@/components/SingleTabGuard";
+import { TooltipProvider } from "@radix-ui/react-tooltip";
+import { ApiProvider } from "./contexts/ApiContext";
+import { HfAuthProvider } from "./contexts/HfAuthContext";
+
+const queryClient = new QueryClient();
+
+function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7b8d89ab36307b0b67b3c6af8fb637655b967a12
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,68 @@
+import React from "react";
+import { Github, BookOpen } from "lucide-react";
+
+const DiscordIcon: React.FC<{ className?: string }> = ({ className }) => (
+
+
+
+);
+
+const links = [
+ {
+ href: "https://github.com/huggingface/lerobot",
+ label: "GitHub",
+ Icon: Github,
+ },
+ {
+ href: "https://huggingface.co/docs/lerobot",
+ label: "Documentation",
+ Icon: BookOpen,
+ },
+ {
+ href: "https://discord.com/invite/s3KuuzsPFb",
+ label: "Discord",
+ Icon: DiscordIcon,
+ },
+];
+
+const Footer: React.FC = () => {
+ return (
+
+ );
+};
+
+export default Footer;
diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e45b6b7209ef5483b501fca534c6c8e63edf41d2
--- /dev/null
+++ b/src/components/Logo.tsx
@@ -0,0 +1,19 @@
+
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+interface LogoProps extends React.HTMLAttributes {
+ iconOnly?: boolean;
+}
+
+const Logo: React.FC = ({
+ className,
+ iconOnly = false
+}) => {
+ return
+
+ {!iconOnly &&
LeLab }
+
;
+};
+
+export default Logo;
diff --git a/src/components/SingleTabGuard.tsx b/src/components/SingleTabGuard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ad4f5b7ae76e105ab33eb009cbad074a918f196e
--- /dev/null
+++ b/src/components/SingleTabGuard.tsx
@@ -0,0 +1,136 @@
+import { useCallback, useEffect, useRef, useState, ReactNode } from "react";
+import { Button } from "@/components/ui/button";
+
+type Peer = { id: string; openedAt: number; lastSeen: number };
+
+const CHANNEL = "lelab-tabs-v1";
+const HEARTBEAT_MS = 1000;
+const PEER_TIMEOUT_MS = 3000;
+
+const SingleTabGuard = ({ children }: { children: ReactNode }) => {
+ const [isPrimary, setIsPrimary] = useState(true);
+ const peersRef = useRef>(new Map());
+ const myIdRef = useRef("");
+ const myOpenedAtRef = useRef(0);
+ const channelRef = useRef(null);
+
+ const recompute = useCallback(() => {
+ const peers = peersRef.current;
+ const cutoff = Date.now() - PEER_TIMEOUT_MS;
+ for (const [id, peer] of peers) {
+ if (peer.lastSeen < cutoff) peers.delete(id);
+ }
+ let winnerId = myIdRef.current;
+ let winnerOpenedAt = myOpenedAtRef.current;
+ for (const peer of peers.values()) {
+ if (
+ peer.openedAt < winnerOpenedAt ||
+ (peer.openedAt === winnerOpenedAt && peer.id < winnerId)
+ ) {
+ winnerId = peer.id;
+ winnerOpenedAt = peer.openedAt;
+ }
+ }
+ setIsPrimary(winnerId === myIdRef.current);
+ }, []);
+
+ useEffect(() => {
+ if (typeof window === "undefined" || typeof BroadcastChannel === "undefined") {
+ return;
+ }
+
+ myIdRef.current = crypto.randomUUID();
+ myOpenedAtRef.current = Date.now();
+
+ const channel = new BroadcastChannel(CHANNEL);
+ channelRef.current = channel;
+
+ const send = (type: string) => {
+ channel.postMessage({
+ type,
+ id: myIdRef.current,
+ openedAt: myOpenedAtRef.current,
+ });
+ };
+
+ channel.onmessage = (e) => {
+ const msg = e.data;
+ if (!msg || msg.id === myIdRef.current) return;
+ const peers = peersRef.current;
+
+ if (msg.type === "HEARTBEAT") {
+ peers.set(msg.id, {
+ id: msg.id,
+ openedAt: msg.openedAt,
+ lastSeen: Date.now(),
+ });
+ } else if (msg.type === "RELEASE") {
+ peers.delete(msg.id);
+ } else if (msg.type === "TAKEOVER") {
+ peers.set(msg.id, {
+ id: msg.id,
+ openedAt: msg.openedAt,
+ lastSeen: Date.now(),
+ });
+ // Move ourselves behind the taker so the election flips.
+ if (myOpenedAtRef.current <= msg.openedAt) {
+ myOpenedAtRef.current = msg.openedAt + 1;
+ }
+ }
+ recompute();
+ };
+
+ send("HEARTBEAT");
+ const interval = setInterval(() => {
+ send("HEARTBEAT");
+ recompute();
+ }, HEARTBEAT_MS);
+
+ const onUnload = () => send("RELEASE");
+ window.addEventListener("beforeunload", onUnload);
+
+ return () => {
+ window.removeEventListener("beforeunload", onUnload);
+ clearInterval(interval);
+ send("RELEASE");
+ channel.close();
+ channelRef.current = null;
+ };
+ }, [recompute]);
+
+ const takeOver = useCallback(() => {
+ myOpenedAtRef.current = 0;
+ channelRef.current?.postMessage({
+ type: "TAKEOVER",
+ id: myIdRef.current,
+ openedAt: 0,
+ });
+ recompute();
+ }, [recompute]);
+
+ return (
+ <>
+ {children}
+ {!isPrimary && (
+
+
+
+ LeLab is already open in another tab
+
+
+ Only one tab can control the robot at a time. Switch back to the
+ original tab, or take over here — the other tab will lock.
+
+
Use this tab
+
+
+ )}
+ >
+ );
+};
+
+export default SingleTabGuard;
diff --git a/src/components/UrdfViewer.tsx b/src/components/UrdfViewer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..60e0b59f1a38fbf9d88c72735af36e0aba9dab3c
--- /dev/null
+++ b/src/components/UrdfViewer.tsx
@@ -0,0 +1,304 @@
+import React, {
+ useEffect,
+ useRef,
+ useState,
+ useMemo,
+ useCallback,
+ memo,
+} from "react";
+import { cn } from "@/lib/utils";
+
+import URDFManipulator from "urdf-loader/src/urdf-manipulator-element.js";
+import { useUrdf } from "@/hooks/useUrdf";
+import { useRealTimeJoints } from "@/hooks/useRealTimeJoints";
+import {
+ createUrdfViewer,
+ setupMeshLoader,
+ setupJointHighlighting,
+ setupModelLoading,
+ URDFViewerElement,
+} from "@/lib/urdfViewerHelpers";
+
+// Register the URDFManipulator as a custom element if it hasn't been already
+if (typeof window !== "undefined" && !customElements.get("urdf-viewer")) {
+ customElements.define("urdf-viewer", URDFManipulator);
+}
+import * as THREE from "three";
+
+// Extend the interface for the URDF viewer element to include background property
+interface UrdfViewerElement extends HTMLElement {
+ background?: string;
+ setJointValue?: (jointName: string, value: number) => void;
+}
+
+const UrdfViewer: React.FC = () => {
+ const containerRef = useRef(null);
+ const [highlightedJoint, setHighlightedJoint] = useState(null);
+ const { registerUrdfProcessor, alternativeUrdfModels, isDefaultModel } =
+ useUrdf();
+
+ const cleanupAnimationRef = useRef<(() => void) | null>(null);
+ const viewerRef = useRef(null);
+ const hasInitializedRef = useRef(false);
+
+ // Real-time joint updates via WebSocket
+ const { isConnected: isWebSocketConnected } = useRealTimeJoints({
+ viewerRef,
+ enabled: isDefaultModel, // Only enable WebSocket for default model
+ });
+
+ // Add state for custom URDF path
+ const [customUrdfPath, setCustomUrdfPath] = useState(null);
+ const [urlModifierFunc, setUrlModifierFunc] = useState<
+ ((url: string) => string) | null
+ >(null);
+
+ const packageRef = useRef("");
+
+ // Implement UrdfProcessor interface for drag and drop
+ const urdfProcessor = useMemo(
+ () => ({
+ loadUrdf: (urdfPath: string) => {
+ setCustomUrdfPath(urdfPath);
+ },
+ setUrlModifierFunc: (func: (url: string) => string) => {
+ setUrlModifierFunc(() => func);
+ },
+ getPackage: () => {
+ return packageRef.current;
+ },
+ }),
+ []
+ );
+
+ // Register the URDF processor with the global drag and drop context
+ useEffect(() => {
+ registerUrdfProcessor(urdfProcessor);
+ }, [registerUrdfProcessor, urdfProcessor]);
+
+ // Create URL modifier function for default model
+ const defaultUrlModifier = useCallback((url: string) => {
+ console.log(`🔗 defaultUrlModifier called with: ${url}`);
+
+ // Handle various package:// URL formats for the default SO-101 model
+ if (url.startsWith("package://so_arm_description/meshes/")) {
+ const modifiedUrl = url.replace(
+ "package://so_arm_description/meshes/",
+ "/so-101-urdf/meshes/"
+ );
+ console.log(`🔗 Modified URL (package): ${modifiedUrl}`);
+ return modifiedUrl;
+ }
+
+ // Handle case where package path might be partially resolved
+ if (url.includes("so_arm_description/meshes/")) {
+ const modifiedUrl = url.replace(
+ /.*so_arm_description\/meshes\//,
+ "/so-101-urdf/meshes/"
+ );
+ console.log(`🔗 Modified URL (partial): ${modifiedUrl}`);
+ return modifiedUrl;
+ }
+
+ // Handle the specific problematic path pattern we're seeing in logs
+ if (url.includes("/so-101-urdf/so_arm_description/meshes/")) {
+ const modifiedUrl = url.replace(
+ "/so-101-urdf/so_arm_description/meshes/",
+ "/so-101-urdf/meshes/"
+ );
+ console.log(`🔗 Modified URL (problematic path): ${modifiedUrl}`);
+ return modifiedUrl;
+ }
+
+ // Handle relative paths that might need mesh folder prefix
+ if (
+ url.endsWith(".stl") &&
+ !url.startsWith("/") &&
+ !url.startsWith("http")
+ ) {
+ const modifiedUrl = `/so-101-urdf/meshes/${url}`;
+ console.log(`🔗 Modified URL (relative): ${modifiedUrl}`);
+ return modifiedUrl;
+ }
+
+ console.log(`🔗 Unmodified URL: ${url}`);
+ return url;
+ }, []);
+
+ // Main effect to create and setup the viewer only once
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ // Create and configure the URDF viewer element
+ const viewer = createUrdfViewer(containerRef.current, true);
+ viewerRef.current = viewer; // Store reference to the viewer
+
+ // Setup mesh loading function with appropriate URL modifier
+ const activeUrlModifier = isDefaultModel
+ ? defaultUrlModifier
+ : urlModifierFunc;
+ setupMeshLoader(viewer, activeUrlModifier);
+
+ // Determine which URDF to load - fixed path to match the actual available file
+ const urdfPath = isDefaultModel
+ ? "/so-101-urdf/urdf/so101_new_calib.urdf"
+ : customUrdfPath || "";
+
+ // Set the package path for the default model
+ if (isDefaultModel) {
+ packageRef.current = "/"; // Set to root so we can handle full path resolution in URL modifier
+ }
+
+ // Setup model loading if a path is available
+ let cleanupModelLoading = () => {};
+ if (urdfPath) {
+ cleanupModelLoading = setupModelLoading(
+ viewer,
+ urdfPath,
+ packageRef.current,
+ setCustomUrdfPath,
+ alternativeUrdfModels
+ );
+ }
+
+ // Setup joint highlighting
+ const cleanupJointHighlighting = setupJointHighlighting(
+ viewer,
+ setHighlightedJoint
+ );
+
+ // Function to fit the robot to the camera view
+ const fitRobotToView = (viewer: URDFViewerElement) => {
+ if (!viewer || !viewer.robot) {
+ console.log(
+ "[RobotViewer] Cannot fit to view: No viewer or robot available"
+ );
+ return;
+ }
+
+ try {
+ // Create a bounding box for the robot
+ const boundingBox = new THREE.Box3().setFromObject(viewer.robot);
+
+ // Calculate the center of the bounding box
+ const center = new THREE.Vector3();
+ boundingBox.getCenter(center);
+
+ // Calculate the size of the bounding box
+ const size = new THREE.Vector3();
+ boundingBox.getSize(size);
+
+ // Get the maximum dimension to ensure the entire robot is visible
+ const maxDim = Math.max(size.x, size.y, size.z);
+
+ // Position camera to see the center of the model
+ viewer.camera.position.copy(center);
+
+ // Move the camera back to see the entire robot
+ // Use the model's up direction to determine which axis to move along
+ const upVector = new THREE.Vector3();
+ if (viewer.up === "+Z" || viewer.up === "Z") {
+ upVector.set(1, 1, 1); // Move back in a diagonal
+ } else if (viewer.up === "+Y" || viewer.up === "Y") {
+ upVector.set(1, 1, 1); // Move back in a diagonal
+ } else {
+ upVector.set(1, 1, 1); // Default direction
+ }
+
+ // Normalize the vector and multiply by the size
+ upVector.normalize().multiplyScalar(maxDim * 1.3);
+ viewer.camera.position.add(upVector);
+
+ // Make the camera look at the center of the model
+ viewer.controls.target.copy(center);
+
+ // Update controls and mark for redraw
+ viewer.controls.update();
+ viewer.redraw();
+
+ console.log("[RobotViewer] Robot auto-fitted to view");
+ } catch (error) {
+ console.error("[RobotViewer] Error fitting robot to view:", error);
+ }
+ };
+
+ // Add event listener for when the robot is loaded to auto-fit to view
+ const onRobotLoad = () => {
+ fitRobotToView(viewer);
+ };
+
+ // Setup animation event handler for the default model or when hasAnimation is true
+ const onModelProcessed = () => {
+ hasInitializedRef.current = true;
+ if ("setJointValue" in viewer) {
+ // Clear any existing animation
+ if (cleanupAnimationRef.current) {
+ cleanupAnimationRef.current();
+ cleanupAnimationRef.current = null;
+ }
+ }
+ // Auto-fit the robot to view when the model is processed
+ onRobotLoad();
+ };
+
+ viewer.addEventListener("urdf-processed", onModelProcessed);
+
+ // Return cleanup function
+ return () => {
+ if (cleanupAnimationRef.current) {
+ cleanupAnimationRef.current();
+ cleanupAnimationRef.current = null;
+ }
+ hasInitializedRef.current = false;
+ cleanupJointHighlighting();
+ cleanupModelLoading();
+ viewer.removeEventListener("urdf-processed", onModelProcessed);
+ };
+ }, [
+ isDefaultModel,
+ customUrdfPath,
+ urlModifierFunc,
+ defaultUrlModifier,
+ alternativeUrdfModels,
+ ]);
+
+ return (
+
+
+
+ {/* Joint highlight indicator */}
+ {highlightedJoint && (
+
+ Joint: {highlightedJoint}
+
+ )}
+
+ {/* WebSocket connection status */}
+ {isDefaultModel && (
+
+
+
+ {isWebSocketConnected ? "Live Robot Data" : "Disconnected"}
+
+
+ )}
+
+ );
+};
+
+export default memo(UrdfViewer);
diff --git a/src/components/control/CommandBar.tsx b/src/components/control/CommandBar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ea5032d6d0330981e08f39b4ce460da6d1918024
--- /dev/null
+++ b/src/components/control/CommandBar.tsx
@@ -0,0 +1,81 @@
+
+import React from 'react';
+import { Mic, MicOff, Send, Camera } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+
+interface CommandBarProps {
+ command: string;
+ setCommand: (command: string) => void;
+ handleSendCommand: () => void;
+ isVoiceActive: boolean;
+ setIsVoiceActive: (isActive: boolean) => void;
+ showCamera: boolean;
+ setShowCamera: (show: boolean) => void;
+ handleEndSession: () => void;
+}
+
+const CommandBar: React.FC = ({
+ command,
+ setCommand,
+ handleSendCommand,
+ isVoiceActive,
+ setIsVoiceActive,
+ showCamera,
+ setShowCamera,
+ handleEndSession
+}) => {
+ return (
+
+
+ setCommand(e.target.value)}
+ placeholder="Tell the robot what to do..."
+ className="flex-1 bg-gray-800 border-gray-600 text-white placeholder-gray-400 text-lg py-3"
+ onKeyPress={(e) => e.key === 'Enter' && handleSendCommand()}
+ />
+
+
+ Send
+
+
+
+
+
+ setIsVoiceActive(!isVoiceActive)}
+ className={`px-6 py-2 ${
+ isVoiceActive ? 'bg-gray-600 text-white hover:bg-gray-500' : 'bg-gray-800 text-gray-300 hover:bg-gray-700'
+ }`}
+ >
+ {isVoiceActive ? : }
+ Voice Command
+
+
+ setShowCamera(!showCamera)}
+ className={`px-6 py-2 ${
+ showCamera ? 'bg-gray-600 text-white hover:bg-gray-500' : 'bg-gray-800 text-gray-300 hover:bg-gray-700'
+ }`}
+ >
+
+ Show Camera
+
+
+
+ End Session
+
+
+
+
+ );
+};
+
+export default CommandBar;
diff --git a/src/components/control/MetricsPanel.tsx b/src/components/control/MetricsPanel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d66d6c63feacc604de91708e6c0f103cd103f21a
--- /dev/null
+++ b/src/components/control/MetricsPanel.tsx
@@ -0,0 +1,190 @@
+
+import React, { useEffect, useRef } from 'react';
+import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
+import { Camera, MicOff } from 'lucide-react';
+
+interface MetricsPanelProps {
+ activeTab: 'SENSORS' | 'MOTORS';
+ setActiveTab: (tab: 'SENSORS' | 'MOTORS') => void;
+ sensorData: any[];
+ motorData: any[];
+ hasPermissions: boolean;
+ streamRef: React.RefObject;
+ isVoiceActive: boolean;
+ micLevel: number;
+}
+
+const MetricsPanel: React.FC = ({
+ activeTab,
+ setActiveTab,
+ sensorData,
+ motorData,
+ hasPermissions,
+ streamRef,
+ isVoiceActive,
+ micLevel,
+}) => {
+ const sensorVideoRef = useRef(null);
+
+ useEffect(() => {
+ if (activeTab === 'SENSORS' && hasPermissions && sensorVideoRef.current && streamRef.current) {
+ if (sensorVideoRef.current.srcObject !== streamRef.current) {
+ sensorVideoRef.current.srcObject = streamRef.current;
+ }
+ }
+ }, [activeTab, hasPermissions, streamRef]);
+
+ return (
+
+
+ {/* Tab Headers */}
+
+ setActiveTab('MOTORS')}
+ className={`px-6 py-2 rounded-t-lg text-sm sm:text-base ${
+ activeTab === 'MOTORS'
+ ? 'bg-orange-500 text-white'
+ : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
+ }`}
+ >
+ MOTORS
+
+ setActiveTab('SENSORS')}
+ className={`px-6 py-2 rounded-t-lg ml-2 text-sm sm:text-base ${
+ activeTab === 'SENSORS'
+ ? 'bg-orange-500 text-white'
+ : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
+ }`}
+ >
+ SENSORS
+
+
+
+ {/* Chart Content */}
+
+ {activeTab === 'SENSORS' && (
+
+ {/* Webcam Feed */}
+
+
Live Camera Feed
+ {hasPermissions ? (
+
+
+
+ ) : (
+
+
+
+
Camera permission not granted.
+
+
+ )}
+
+
+ {/* Mic Detection & Other Sensors */}
+
+
+
Voice Activity
+ {hasPermissions ? (
+
+
+ {[...Array(15)].map((_, i) => {
+ const barIsActive = isVoiceActive && i < (micLevel / 120 * 15);
+ return (
+
+ );
+ })}
+
+
+ {isVoiceActive ? "Voice commands active" : "Voice commands muted"}
+
+
+ ) : (
+
+
+
+
Microphone permission not granted.
+
+
+ )}
+
+
+ {/* Sensor Charts */}
+ {['sensor3', 'sensor4'].map((sensor, index) => (
+
+
Sensor {index + 3}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {activeTab === 'MOTORS' && (
+
+ {['motor1', 'motor2', 'motor3', 'motor4', 'motor5', 'motor6'].map((motor, index) => (
+
+
Motor {index + 1}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+};
+
+export default MetricsPanel;
diff --git a/src/components/control/VisualizerPanel.tsx b/src/components/control/VisualizerPanel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..93ef0d2a027ff7f579dbcec85632d5f71511f08f
--- /dev/null
+++ b/src/components/control/VisualizerPanel.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { ArrowLeft } from "lucide-react";
+import { cn } from "@/lib/utils";
+import UrdfViewer from "../UrdfViewer";
+import Logo from "@/components/Logo";
+
+interface VisualizerPanelProps {
+ onGoBack: () => void;
+ className?: string;
+}
+
+const VisualizerPanel: React.FC = ({
+ onGoBack,
+ className,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
Teleoperation
+
+
+
+
+
+
+ );
+};
+
+export default VisualizerPanel;
diff --git a/src/components/jobs/CheckpointDropdown.tsx b/src/components/jobs/CheckpointDropdown.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..35d4def8a0bd0cb8c7b1dd5dec1feb474706b4b4
--- /dev/null
+++ b/src/components/jobs/CheckpointDropdown.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { JobCheckpoint } from "@/lib/checkpointsApi";
+
+interface Props {
+ checkpoints: JobCheckpoint[];
+ selectedStep: number | null;
+ onChange: (step: number) => void;
+ disabled?: boolean;
+ placeholder?: string;
+}
+
+export const CheckpointDropdown: React.FC = ({
+ checkpoints,
+ selectedStep,
+ onChange,
+ disabled,
+ placeholder = "Select checkpoint",
+}) => {
+ const value = selectedStep != null ? String(selectedStep) : undefined;
+ return (
+ onChange(Number(v))}
+ disabled={disabled || checkpoints.length === 0}
+ >
+ e.stopPropagation()}
+ >
+
+
+
+ {checkpoints.map((c) => (
+ e.stopPropagation()}
+ >
+ step {c.step}
+
+ ))}
+
+
+ );
+};
+
+export default CheckpointDropdown;
diff --git a/src/components/jobs/HubJobCard.tsx b/src/components/jobs/HubJobCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6f2a34d028e936339793803d38b5ec8b3796902b
--- /dev/null
+++ b/src/components/jobs/HubJobCard.tsx
@@ -0,0 +1,106 @@
+import React from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { HubJob } from "@/lib/jobsApi";
+import {
+ ExternalLink,
+ AlertTriangle,
+ CheckCircle2,
+ Loader2,
+ XCircle,
+ Clock,
+ HelpCircle,
+} from "lucide-react";
+
+interface Props {
+ job: HubJob;
+}
+
+function relativeTime(iso: string | null): string {
+ if (!iso) return "—";
+ const t = Date.parse(iso);
+ if (Number.isNaN(t)) return "—";
+ const diff = Math.max(0, (Date.now() - t) / 1000);
+ if (diff < 60) return `${Math.floor(diff)}s ago`;
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ return `${Math.floor(diff / 86400)}d ago`;
+}
+
+interface StagePresentation {
+ label: string;
+ color: string;
+ Icon: React.ComponentType<{ className?: string }>;
+ spin?: boolean;
+}
+
+const stagePresentation: Record = {
+ RUNNING: { label: "Running", color: "text-green-400", Icon: Loader2, spin: true },
+ QUEUED: { label: "Queued", color: "text-amber-400", Icon: Clock },
+ SCHEDULING: { label: "Scheduling", color: "text-amber-400", Icon: Clock },
+ COMPLETED: { label: "Done", color: "text-slate-400", Icon: CheckCircle2 },
+ FAILED: { label: "Failed", color: "text-red-400", Icon: XCircle },
+ // HF API uses "CANCELED" (single L); accept both spellings.
+ CANCELED: { label: "Cancelled", color: "text-amber-400", Icon: AlertTriangle },
+ CANCELLED: { label: "Cancelled", color: "text-amber-400", Icon: AlertTriangle },
+};
+
+const HubJobCard: React.FC = ({ job }) => {
+ const stage = job.status?.stage?.toUpperCase() ?? "";
+ const present: StagePresentation = stagePresentation[stage] ?? {
+ label: stage || "Unknown",
+ color: "text-slate-400",
+ Icon: HelpCircle,
+ };
+ const Icon = present.Icon;
+ const title =
+ job.docker_image ?? job.space_id ?? `Job ${job.id.slice(0, 12)}…`;
+
+ return (
+ window.open(job.url, "_blank", "noopener,noreferrer")}
+ className="bg-slate-800/50 border-slate-700 rounded-xl cursor-pointer hover:border-slate-500 transition-colors"
+ >
+
+
+
+
+ {title}
+
+
+ {job.flavor ?? "—"} · {relativeTime(job.created_at)}
+ {job.owner ? ` · ${job.owner}` : ""}
+
+
+ {job.status?.message ? (
+
+ {job.status.message}
+
+ ) : null}
+
+
+ );
+};
+
+export default HubJobCard;
diff --git a/src/components/jobs/HubModelCard.tsx b/src/components/jobs/HubModelCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..599a54515fcb95c5ac1612d21a9843d5f518daec
--- /dev/null
+++ b/src/components/jobs/HubModelCard.tsx
@@ -0,0 +1,75 @@
+import React from "react";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { HubModel } from "@/lib/jobsApi";
+import { ExternalLink, Lock, Upload } from "lucide-react";
+
+interface Props {
+ model: HubModel;
+}
+
+function relativeTime(iso: string | null): string {
+ if (!iso) return "—";
+ const t = Date.parse(iso);
+ if (Number.isNaN(t)) return "—";
+ const diff = Math.max(0, (Date.now() - t) / 1000);
+ if (diff < 60) return `${Math.floor(diff)}s ago`;
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ return `${Math.floor(diff / 86400)}d ago`;
+}
+
+const HubModelCard: React.FC = ({ model }) => {
+ const url = `https://huggingface.co/${model.repo_id}`;
+ const shortName = model.repo_id.includes("/")
+ ? model.repo_id.split("/").slice(1).join("/")
+ : model.repo_id;
+
+ return (
+ window.open(url, "_blank", "noopener,noreferrer")}
+ className="bg-slate-800/50 border-slate-700 rounded-xl cursor-pointer hover:border-slate-500 transition-colors"
+ >
+
+
+
+
+ {model.private ? (
+
+ ) : null}
+ {shortName}
+
+
+ {model.repo_id} · updated {relativeTime(model.last_modified)}
+
+
+
+
+ );
+};
+
+export default HubModelCard;
diff --git a/src/components/jobs/JobCard.tsx b/src/components/jobs/JobCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6e3b50b5fba7c0647510b3cc4dc879a38113ff67
--- /dev/null
+++ b/src/components/jobs/JobCard.tsx
@@ -0,0 +1,199 @@
+import React, { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { JobRecord } from "@/lib/jobsApi";
+import {
+ Square,
+ X,
+ AlertTriangle,
+ CheckCircle2,
+ Loader2,
+ XCircle,
+ ExternalLink,
+ Play,
+} from "lucide-react";
+import { useApi } from "@/contexts/ApiContext";
+import {
+ JobCheckpoint,
+ listJobCheckpoints,
+} from "@/lib/checkpointsApi";
+import CheckpointDropdown from "@/components/jobs/CheckpointDropdown";
+
+interface Props {
+ job: JobRecord;
+ onStop: (id: string) => void;
+ onDelete: (id: string) => void;
+ onPlay: (job: JobRecord, step: number) => void;
+}
+
+function relativeTime(epochSec: number): string {
+ const diff = Math.max(0, Date.now() / 1000 - epochSec);
+ if (diff < 60) return `${Math.floor(diff)}s ago`;
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
+ return `${Math.floor(diff / 86400)}d ago`;
+}
+
+const statePresentation: Record<
+ JobRecord["state"],
+ { label: string; color: string; Icon: React.ComponentType<{ className?: string }> }
+> = {
+ running: { label: "Running", color: "text-green-400", Icon: Loader2 },
+ done: { label: "Done", color: "text-slate-400", Icon: CheckCircle2 },
+ failed: { label: "Failed", color: "text-red-400", Icon: XCircle },
+ interrupted: { label: "Interrupted", color: "text-amber-400", Icon: AlertTriangle },
+};
+
+const JobCard: React.FC = ({ job, onStop, onDelete, onPlay }) => {
+ const navigate = useNavigate();
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const present = statePresentation[job.state];
+ const Icon = present.Icon;
+ const isRunning = job.state === "running";
+ const isStarting = isRunning && job.metrics.total_steps === 0;
+ const progressPct =
+ job.metrics.total_steps > 0
+ ? Math.min(100, (job.metrics.current_step / job.metrics.total_steps) * 100)
+ : 0;
+
+ const subtitle = isStarting
+ ? "starting…"
+ : isRunning
+ ? `started ${relativeTime(job.started_at)}`
+ : job.ended_at != null
+ ? `ended ${relativeTime(job.ended_at)}`
+ : present.label.toLowerCase();
+
+ const [checkpoints, setCheckpoints] = useState([]);
+ const [selectedStep, setSelectedStep] = useState(null);
+
+ useEffect(() => {
+ if (job.checkpoint_count <= 0) {
+ setCheckpoints([]);
+ setSelectedStep(null);
+ return;
+ }
+ let cancelled = false;
+ listJobCheckpoints(baseUrl, fetchWithHeaders, job.id)
+ .then((cks) => {
+ if (cancelled) return;
+ setCheckpoints(cks);
+ if (cks.length > 0) {
+ const latest = cks[cks.length - 1].step;
+ setSelectedStep((prev) =>
+ prev != null && cks.some((c) => c.step === prev) ? prev : latest,
+ );
+ } else {
+ setSelectedStep(null);
+ }
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setCheckpoints([]);
+ setSelectedStep(null);
+ }
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [baseUrl, fetchWithHeaders, job.id, job.checkpoint_count]);
+
+ const handleAction = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (isRunning) {
+ if (window.confirm("Stop this run?")) onStop(job.id);
+ } else {
+ if (window.confirm("Delete this run? This wipes the output directory.")) onDelete(job.id);
+ }
+ };
+
+ const handlePlay = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (selectedStep == null) return;
+ onPlay(job, selectedStep);
+ };
+
+ const showProgressBar = isRunning;
+ const showInferenceRow = checkpoints.length > 0 && selectedStep != null;
+
+ return (
+ navigate(`/training/${job.id}`)}
+ className="bg-slate-800/50 border-slate-700 rounded-xl cursor-pointer hover:border-slate-500 transition-colors"
+ >
+
+
+
+
+ {present.label}
+
+ {job.runner === "hf_cloud" && job.hf_job_url ? (
+
+ e.stopPropagation()}
+ >
+
+
+
+ ) : (
+
+ {isRunning ? : }
+
+ )}
+
+
+
+ {job.name}
+
+
{subtitle}
+
+ {showProgressBar ? (
+
+
+
+ {isStarting ? "Training starting…" : `${progressPct.toFixed(1)}%`}
+
+
+ ) : null}
+ {showInferenceRow ? (
+
+ ) : null}
+
+
+ );
+};
+
+export default JobCard;
diff --git a/src/components/jobs/JobsSection.tsx b/src/components/jobs/JobsSection.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..30817e3fae85c98c56a664dcb748e19cd666f89b
--- /dev/null
+++ b/src/components/jobs/JobsSection.tsx
@@ -0,0 +1,379 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { useApi } from "@/contexts/ApiContext";
+import { useToast } from "@/hooks/use-toast";
+import { useJobsChangedSignal } from "@/hooks/useJobsChangedSignal";
+import {
+ HubJob,
+ HubModel,
+ JobProgressSnapshot,
+ JobRecord,
+ deleteJob,
+ listHubJobs,
+ listJobs,
+ stopJob,
+} from "@/lib/jobsApi";
+import JobCard from "./JobCard";
+import HubJobCard from "./HubJobCard";
+import HubModelCard from "./HubModelCard";
+import InferenceModal from "@/components/landing/InferenceModal";
+import { useRobots } from "@/hooks/useRobots";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { ChevronRight, RefreshCw, Search } from "lucide-react";
+
+const LIMIT = 10;
+
+// Hub stages still doing work. Anything outside this set (COMPLETED, FAILED,
+// CANCELED, …) gets demoted to UNTRACKED.
+const HUB_ACTIVE_STAGES = new Set(["RUNNING", "QUEUED", "SCHEDULING"]);
+
+const isJobActive = (j: JobRecord) =>
+ j.state === "running" || j.checkpoint_count > 0;
+
+const isHubJobActive = (h: HubJob) =>
+ HUB_ACTIVE_STAGES.has((h.status?.stage ?? "").toUpperCase());
+
+const JobsSection: React.FC = () => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const { toast } = useToast();
+
+ const [jobs, setJobs] = useState([]);
+ const [hubJobs, setHubJobs] = useState([]);
+ const [hubModels, setHubModels] = useState([]);
+ const [hubAuthenticated, setHubAuthenticated] = useState(false);
+ const [error, setError] = useState(null);
+ const [search, setSearch] = useState("");
+
+ const { selectedRecord } = useRobots();
+ const [inferenceModalOpen, setInferenceModalOpen] = useState(false);
+ const [inferenceJob, setInferenceJob] = useState(null);
+ const [inferenceStep, setInferenceStep] = useState(null);
+
+ const refresh = useCallback(async () => {
+ try {
+ const [next, hub] = await Promise.all([
+ listJobs(baseUrl, fetchWithHeaders, LIMIT),
+ listHubJobs(baseUrl, fetchWithHeaders),
+ ]);
+ setJobs(next);
+ setHubJobs(hub.jobs);
+ setHubModels(hub.models);
+ setHubAuthenticated(hub.authenticated);
+ setError(null);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : String(e));
+ }
+ }, [baseUrl, fetchWithHeaders]);
+
+ // Initial fetch on mount + refetch when the tab regains focus. Backend
+ // pushes a `jobs_changed` WS event on every registry mutation, which
+ // covers any change originating on this machine. The focus refresh
+ // catches changes originating elsewhere (e.g. a job submitted from
+ // another tab or the HF dashboard) without burning the rate limit.
+ useEffect(() => {
+ refresh();
+ const onVisible = () => {
+ if (document.visibilityState === "visible") refresh();
+ };
+ document.addEventListener("visibilitychange", onVisible);
+ window.addEventListener("focus", refresh);
+ return () => {
+ document.removeEventListener("visibilitychange", onVisible);
+ window.removeEventListener("focus", refresh);
+ };
+ }, [refresh]);
+
+ const applyProgress = useCallback((snapshots: JobProgressSnapshot[]) => {
+ if (snapshots.length === 0) return;
+ setJobs((prev) => {
+ if (prev.length === 0) return prev;
+ const byId = new Map(snapshots.map((s) => [s.id, s]));
+ let mutated = false;
+ const next = prev.map((j) => {
+ const s = byId.get(j.id);
+ if (!s) return j;
+ mutated = true;
+ return {
+ ...j,
+ state: s.state,
+ metrics: s.metrics,
+ wandb_run_url: s.wandb_run_url,
+ checkpoint_count: s.checkpoint_count,
+ };
+ });
+ return mutated ? next : prev;
+ });
+ }, []);
+
+ useJobsChangedSignal(refresh, applyProgress);
+
+ const handleStop = async (id: string) => {
+ try {
+ await stopJob(baseUrl, fetchWithHeaders, id);
+ toast({ title: "Job stopping" });
+ refresh();
+ } catch (e) {
+ toast({
+ title: "Stop failed",
+ description: e instanceof Error ? e.message : String(e),
+ variant: "destructive",
+ });
+ }
+ };
+
+ const handlePlay = (job: JobRecord, step: number) => {
+ setInferenceJob(job);
+ setInferenceStep(step);
+ setInferenceModalOpen(true);
+ };
+
+ const handleDelete = async (id: string) => {
+ try {
+ await deleteJob(baseUrl, fetchWithHeaders, id);
+ toast({ title: "Job removed" });
+ refresh();
+ } catch (e) {
+ toast({
+ title: "Delete failed",
+ description: e instanceof Error ? e.message : String(e),
+ variant: "destructive",
+ });
+ }
+ };
+
+ const query = search.trim().toLowerCase();
+ const matchesQuery = useCallback(
+ (text: string | null | undefined) =>
+ !query || (text ?? "").toLowerCase().includes(query),
+ [query],
+ );
+
+ const filteredJobs = useMemo(
+ () => jobs.filter((j) => matchesQuery(j.name)),
+ [jobs, matchesQuery],
+ );
+ const filteredHubJobs = useMemo(
+ () =>
+ hubJobs.filter((h) =>
+ matchesQuery(h.docker_image ?? h.space_id ?? h.id),
+ ),
+ [hubJobs, matchesQuery],
+ );
+ const filteredHubModels = useMemo(
+ () => hubModels.filter((m) => matchesQuery(m.repo_id)),
+ [hubModels, matchesQuery],
+ );
+
+ const localJobs = useMemo(
+ () => filteredJobs.filter((j) => j.runner === "local"),
+ [filteredJobs],
+ );
+ const trackedCloudJobs = useMemo(
+ () => filteredJobs.filter((j) => j.runner === "hf_cloud"),
+ [filteredJobs],
+ );
+ // Hub jobs already mirrored by a local JobRecord get their richer card via
+ // trackedCloudJobs; everything else from the hub gets a plain HubJobCard.
+ const trackedHfJobIds = useMemo(
+ () =>
+ new Set(
+ trackedCloudJobs
+ .map((j) => j.hf_job_id)
+ .filter((id): id is string => !!id),
+ ),
+ [trackedCloudJobs],
+ );
+ const untrackedHubJobs = useMemo(
+ () => filteredHubJobs.filter((h) => !trackedHfJobIds.has(h.id)),
+ [filteredHubJobs, trackedHfJobIds],
+ );
+ // Hide model repos that map 1-to-1 to a tracked cloud job (those already
+ // appear via JobCard); the remainder are past trainings the registry no
+ // longer remembers.
+ const trackedRepoIds = useMemo(
+ () =>
+ new Set(
+ trackedCloudJobs
+ .map((j) => j.hf_repo_id)
+ .filter((id): id is string => !!id),
+ ),
+ [trackedCloudJobs],
+ );
+ const untrackedHubModels = useMemo(
+ () => filteredHubModels.filter((m) => !trackedRepoIds.has(m.repo_id)),
+ [filteredHubModels, trackedRepoIds],
+ );
+
+ // Active = running or has runnable checkpoints. Everything else collapses
+ // under UNTRACKED so the eye lands on what's still relevant.
+ const localActive = useMemo(() => localJobs.filter(isJobActive), [localJobs]);
+ const localUntracked = useMemo(
+ () => localJobs.filter((j) => !isJobActive(j)),
+ [localJobs],
+ );
+ const trackedCloudActive = useMemo(
+ () => trackedCloudJobs.filter(isJobActive),
+ [trackedCloudJobs],
+ );
+ const trackedCloudUntracked = useMemo(
+ () => trackedCloudJobs.filter((j) => !isJobActive(j)),
+ [trackedCloudJobs],
+ );
+ const untrackedHubActive = useMemo(
+ () => untrackedHubJobs.filter(isHubJobActive),
+ [untrackedHubJobs],
+ );
+ const untrackedHubInactive = useMemo(
+ () => untrackedHubJobs.filter((h) => !isHubJobActive(h)),
+ [untrackedHubJobs],
+ );
+
+ const untrackedCount =
+ localUntracked.length +
+ trackedCloudUntracked.length +
+ untrackedHubInactive.length;
+
+ return (
+
+
+
Jobs
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search jobs"
+ className="h-8 w-48 sm:w-60 pl-8 bg-slate-800/50 border-slate-700 text-sm text-white placeholder:text-slate-500"
+ aria-label="Search jobs"
+ />
+
+
+
+
+
+
+
+ {error ? Couldn't load jobs: {error}
: null}
+
+
+
+ Local jobs
+
+ {localActive.length === 0 ? (
+
+ {query
+ ? "No local jobs match your search."
+ : "No active local jobs. Start one from the Training page."}
+
+ ) : (
+
+ {localActive.map((job) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ Online jobs
+
+ {!hubAuthenticated && trackedCloudJobs.length === 0 ? (
+
+ Sign in with Hugging Face to see your cloud jobs.
+
+ ) : trackedCloudActive.length === 0 &&
+ untrackedHubActive.length === 0 &&
+ untrackedHubModels.length === 0 ? (
+
+ {query ? "No online jobs match your search." : "No active cloud jobs."}
+
+ ) : (
+
+ {trackedCloudActive.map((job) => (
+
+ ))}
+ {untrackedHubActive.map((job) => (
+
+ ))}
+ {untrackedHubModels.map((model) => (
+
+ ))}
+
+ )}
+
+
+ {untrackedCount > 0 ? (
+
+
+
+ Untracked ({untrackedCount})
+
+
+
+ {localUntracked.map((job) => (
+
+ ))}
+ {trackedCloudUntracked.map((job) => (
+
+ ))}
+ {untrackedHubInactive.map((job) => (
+
+ ))}
+
+
+
+ ) : null}
+
+ {inferenceJob ? (
+
+ ) : null}
+
+ );
+};
+
+export default JobsSection;
diff --git a/src/components/landing/DatasetPicker.tsx b/src/components/landing/DatasetPicker.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..aca33fef057dbf4f1f494dcc93d20a547ce99f85
--- /dev/null
+++ b/src/components/landing/DatasetPicker.tsx
@@ -0,0 +1,177 @@
+import React, { useState } from "react";
+import { Plus, ExternalLink } from "lucide-react";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import { DatasetItem } from "@/lib/replayApi";
+
+interface DatasetPickerProps {
+ datasets: DatasetItem[];
+ loading: boolean;
+ onPickExisting: (item: DatasetItem) => void;
+ onCreateNew: (name: string) => void;
+ onOpenCustom: (repoId: string) => void;
+ children: React.ReactNode;
+}
+
+const REPO_ID_RE = /^[\w.\-]+\/[\w.\-]+$/;
+const NAME_RE = /^[A-Za-z0-9._-]+$/;
+
+const DatasetPicker: React.FC = ({
+ datasets,
+ loading,
+ onPickExisting,
+ onCreateNew,
+ onOpenCustom,
+ children,
+}) => {
+ const [open, setOpen] = useState(false);
+ const [query, setQuery] = useState("");
+
+ const trimmed = query.trim();
+ const matchesExisting = datasets.some(
+ (d) => d.repo_id.toLowerCase() === trimmed.toLowerCase(),
+ );
+ const isRepoId = REPO_ID_RE.test(trimmed);
+ const isName = NAME_RE.test(trimmed) && !trimmed.includes("/");
+ const canCreate = trimmed.length > 0 && isName && !matchesExisting;
+ const canOpenCustom = isRepoId && !matchesExisting;
+
+ const createDisabled = matchesExisting || (trimmed !== "" && !canCreate);
+ const createLabel = matchesExisting
+ ? "Already exists"
+ : trimmed === ""
+ ? "Create new dataset…"
+ : canCreate
+ ? `Create "${trimmed}"`
+ : 'Use a name without "/"';
+
+ const handleFooterCreate = () => {
+ if (createDisabled) return;
+ onCreateNew(trimmed);
+ reset();
+ };
+
+ const localDatasets = datasets.filter((d) => d.source === "local" || d.source === "both");
+ const hubDatasets = datasets.filter((d) => d.source === "hub");
+
+ const reset = () => {
+ setQuery("");
+ setOpen(false);
+ };
+
+ const handlePick = (item: DatasetItem) => {
+ onPickExisting(item);
+ reset();
+ };
+
+ const handleCreate = () => {
+ if (!canCreate) return;
+ onCreateNew(trimmed);
+ reset();
+ };
+
+ const handleOpenCustom = () => {
+ if (!canOpenCustom) return;
+ onOpenCustom(trimmed);
+ reset();
+ };
+
+ const renderItem = (d: DatasetItem) => (
+ handlePick(d)}
+ className="text-white aria-selected:bg-gray-700"
+ >
+ {d.repo_id}
+ {d.source === "both" && (
+ on Hub
+ )}
+ {d.private && (
+ private
+ )}
+
+ );
+
+ return (
+
+ {children}
+
+
+ setQuery(v.replace(/[^A-Za-z0-9._\-/]/g, "_"))}
+ onKeyDown={(e) => {
+ if (e.key !== "Enter") return;
+ if (canCreate) {
+ e.preventDefault();
+ handleCreate();
+ } else if (canOpenCustom) {
+ e.preventDefault();
+ handleOpenCustom();
+ }
+ }}
+ className="text-white"
+ />
+
+ {datasets.length === 0 && !canCreate && !canOpenCustom && (
+
+ {loading
+ ? "Loading datasets…"
+ : "No datasets yet. Type a name to create one."}
+
+ )}
+ {localDatasets.length > 0 && (
+
+ {localDatasets.map(renderItem)}
+
+ )}
+ {hubDatasets.length > 0 && (
+
+ {hubDatasets.map(renderItem)}
+
+ )}
+ {canOpenCustom && (
+
+
+
+ Open "{trimmed}" in viewer
+
+
+ )}
+
+
+
+ {createLabel}
+
+
+
+
+ );
+};
+
+export default DatasetPicker;
diff --git a/src/components/landing/HfAuthBanner.tsx b/src/components/landing/HfAuthBanner.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a391ec1692dbc7045a394eaeaebb2d575aa50e61
--- /dev/null
+++ b/src/components/landing/HfAuthBanner.tsx
@@ -0,0 +1,107 @@
+import React, { useState } from "react";
+import { AlertCircle, ExternalLink, Loader2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { useApi } from "@/contexts/ApiContext";
+import { useHfAuth } from "@/contexts/HfAuthContext";
+
+const HfAuthBanner: React.FC = () => {
+ const { auth, refetch } = useHfAuth();
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const [token, setToken] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ if (auth.status === "authenticated" || auth.status === "loading") {
+ return null;
+ }
+
+ const handleSave = async () => {
+ const trimmed = token.trim();
+ if (!trimmed) return;
+ setSubmitting(true);
+ setError(null);
+ try {
+ const r = await fetchWithHeaders(`${baseUrl}/hf-auth/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token: trimmed }),
+ });
+ if (!r.ok) {
+ const body = await r.json().catch(() => ({}));
+ throw new Error(body.detail || `HTTP ${r.status}`);
+ }
+ setToken("");
+ await refetch();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : String(e));
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ Hugging Face access required for cloud training
+
+
+ Create a token at{" "}
+
+ huggingface.co/settings/tokens
+
+
+ {" "}with Write access (so trained
+ policies can upload to your account), then paste it below.
+
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+ );
+};
+
+export default HfAuthBanner;
diff --git a/src/components/landing/HfAuthChip.tsx b/src/components/landing/HfAuthChip.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c17b0ce6ac3a15fc929d602d5e1bdf8280d93410
--- /dev/null
+++ b/src/components/landing/HfAuthChip.tsx
@@ -0,0 +1,54 @@
+import React, { useState } from "react";
+import { Loader2 } from "lucide-react";
+import { useHfAuth } from "@/contexts/HfAuthContext";
+import HfAuthDialog from "./HfAuthDialog";
+
+const HfAuthChip: React.FC = () => {
+ const { auth } = useHfAuth();
+ const [dialogOpen, setDialogOpen] = useState(false);
+
+ if (auth.status === "loading") {
+ return (
+
+
+ Checking HF…
+
+ );
+ }
+
+ if (auth.status === "authenticated") {
+ return (
+
+
+ {auth.username}
+
+ );
+ }
+
+ // unauthenticated
+ return (
+ <>
+ setDialogOpen(true)}
+ className="inline-flex items-center gap-2 rounded-full border border-amber-700/60 bg-amber-950/40 px-3 py-1 text-xs text-amber-100 hover:bg-amber-900/40 transition-colors"
+ aria-label="Hugging Face not configured — show login instructions"
+ >
+
+ HF not configured
+
+
+ >
+ );
+};
+
+export default HfAuthChip;
diff --git a/src/components/landing/HfAuthDialog.tsx b/src/components/landing/HfAuthDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f75693ca2adeaf85aaba592107dc2daf540ee8a7
--- /dev/null
+++ b/src/components/landing/HfAuthDialog.tsx
@@ -0,0 +1,90 @@
+import React, { useState } from "react";
+import { Check, Copy, RefreshCw } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { useHfAuth } from "@/contexts/HfAuthContext";
+
+interface HfAuthDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+const HfAuthDialog: React.FC = ({ open, onOpenChange }) => {
+ const { auth, refetch } = useHfAuth();
+ const [copied, setCopied] = useState(false);
+ const [refetching, setRefetching] = useState(false);
+
+ if (auth.status !== "unauthenticated") {
+ return null;
+ }
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(auth.loginCommand);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ } catch (err) {
+ console.warn("Clipboard write failed:", err);
+ }
+ };
+
+ const handleRefetch = async () => {
+ setRefetching(true);
+ try {
+ await refetch();
+ } finally {
+ setRefetching(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Hugging Face CLI not configured
+
+
+ Uploads, training, and replay-from-Hub require a logged-in HF CLI.
+ Run this in a terminal:
+
+
+
+ {auth.loginCommand}
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ I've logged in — recheck
+
+
+
+ );
+};
+
+export default HfAuthDialog;
diff --git a/src/components/landing/InferenceModal.tsx b/src/components/landing/InferenceModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e60527e38b4bd7b46883582819859113b8021cab
--- /dev/null
+++ b/src/components/landing/InferenceModal.tsx
@@ -0,0 +1,459 @@
+import React, { useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { NumberInput } from "@/components/ui/number-input";
+import { Label } from "@/components/ui/label";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from "@/components/ui/dialog";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { AlertTriangle, CheckCircle, Loader2, Play, VideoOff } from "lucide-react";
+import { RobotRecord } from "@/hooks/useRobots";
+import { useApi } from "@/contexts/ApiContext";
+import { useToast } from "@/hooks/use-toast";
+import { useNavigate } from "react-router-dom";
+import {
+ JobCheckpoint,
+ PolicyConfigSummary,
+ getCheckpointPolicyConfig,
+ listJobCheckpoints,
+} from "@/lib/checkpointsApi";
+import { startInference } from "@/lib/inferenceApi";
+import CheckpointDropdown from "@/components/jobs/CheckpointDropdown";
+import { useAvailableCameras } from "@/hooks/useAvailableCameras";
+import { useCameraStream } from "@/hooks/useCameraStream";
+
+const CameraThumbnail: React.FC<{ deviceId: string; paused: boolean }> = ({
+ deviceId,
+ paused,
+}) => {
+ const { videoRef, hasError } = useCameraStream(deviceId, paused);
+ if (paused || hasError || !deviceId) {
+ return (
+
+
+
+ {paused ? "Released" : "No preview"}
+
+
+ );
+ }
+ return (
+
+ );
+};
+
+interface Props {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ robot: RobotRecord | null;
+ jobId: string;
+ initialStep: number | null;
+}
+
+const DEFAULT_FPS = 30;
+
+const InferenceModal: React.FC = ({
+ open,
+ onOpenChange,
+ robot,
+ jobId,
+ initialStep,
+}) => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const { toast } = useToast();
+ const navigate = useNavigate();
+
+ const [checkpoints, setCheckpoints] = useState([]);
+ const [selectedStep, setSelectedStep] = useState(initialStep);
+ const [task, setTask] = useState("");
+ const [durationS, setDurationS] = useState(60);
+ const [submitting, setSubmitting] = useState(false);
+
+ const [policyConfig, setPolicyConfig] = useState(null);
+ const [policyConfigLoading, setPolicyConfigLoading] = useState(false);
+ const [policyConfigError, setPolicyConfigError] = useState(null);
+
+ // Per expected camera name → user-selected physical camera index (or null).
+ const [cameraBindings, setCameraBindings] = useState>({});
+ const { cameras: availableCameras } = useAvailableCameras({ enabled: open });
+
+ // Load checkpoints when modal opens.
+ useEffect(() => {
+ if (!open) return;
+ let cancelled = false;
+ listJobCheckpoints(baseUrl, fetchWithHeaders, jobId)
+ .then((cks) => {
+ if (cancelled) return;
+ setCheckpoints(cks);
+ if (cks.length > 0) {
+ const latest = cks[cks.length - 1].step;
+ setSelectedStep((prev) => (prev != null ? prev : latest));
+ }
+ })
+ .catch(() => {
+ if (cancelled) return;
+ setCheckpoints([]);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [open, baseUrl, fetchWithHeaders, jobId]);
+
+
+ // Load policy config when step changes.
+ useEffect(() => {
+ if (!open || selectedStep == null) {
+ setPolicyConfig(null);
+ setPolicyConfigError(null);
+ return;
+ }
+ let cancelled = false;
+ setPolicyConfigLoading(true);
+ setPolicyConfigError(null);
+ getCheckpointPolicyConfig(baseUrl, fetchWithHeaders, jobId, selectedStep)
+ .then((cfg) => {
+ if (cancelled) return;
+ setPolicyConfig(cfg);
+ // Reset camera bindings to one entry per expected camera name.
+ // Preserve any prior selection that's still relevant.
+ setCameraBindings((prev) => {
+ const next: Record = {};
+ for (const name of Object.keys(cfg.image_features)) {
+ next[name] = prev[name] ?? null;
+ }
+ return next;
+ });
+ })
+ .catch((e) => {
+ if (cancelled) return;
+ setPolicyConfig(null);
+ setPolicyConfigError(e instanceof Error ? e.message : String(e));
+ })
+ .finally(() => {
+ if (!cancelled) setPolicyConfigLoading(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [open, baseUrl, fetchWithHeaders, jobId, selectedStep]);
+
+ // If the selected robot has cameras whose names match a policy-expected
+ // camera, auto-bind them. Prefer matching by browser device_id (stable
+ // across cv2 index drift); fall back to the saved camera_index.
+ useEffect(() => {
+ if (!policyConfig) return;
+ const robotCams = robot?.cameras ?? [];
+ if (robotCams.length === 0 || availableCameras.length === 0) return;
+ setCameraBindings((prev) => {
+ let changed = false;
+ const next = { ...prev };
+ for (const policyName of Object.keys(policyConfig.image_features)) {
+ if (next[policyName] != null) continue;
+ const robotCam = robotCams.find(
+ (c) => c.name.toLowerCase() === policyName.toLowerCase(),
+ );
+ if (!robotCam) continue;
+ const live =
+ (robotCam.device_id &&
+ availableCameras.find((c) => c.deviceId === robotCam.device_id)) ||
+ availableCameras.find((c) => c.index === robotCam.camera_index);
+ if (live) {
+ next[policyName] = live.index;
+ changed = true;
+ }
+ }
+ return changed ? next : prev;
+ });
+ }, [policyConfig, robot, availableCameras]);
+
+ const selectedRef =
+ selectedStep != null
+ ? checkpoints.find((c) => c.step === selectedStep)?.ref ?? null
+ : null;
+
+ const expectedCameraNames = policyConfig
+ ? Object.keys(policyConfig.image_features)
+ : [];
+ const allCamerasBound = expectedCameraNames.every(
+ (name) => cameraBindings[name] != null,
+ );
+
+ const canStart =
+ !!robot &&
+ robot.is_clean &&
+ selectedRef != null &&
+ !!policyConfig &&
+ allCamerasBound &&
+ !submitting;
+
+ const handleStart = async () => {
+ if (!robot || selectedRef == null || !policyConfig) return;
+ // Setting submitting=true makes every CameraPreview drop its
+ // browser stream — required so the rollout subprocess can open the
+ // same camera index via OpenCV without colliding on the device.
+ setSubmitting(true);
+ await new Promise((r) => setTimeout(r, 300));
+ const cameraDict: Record = {};
+ for (const [name, dims] of Object.entries(policyConfig.image_features)) {
+ const idx = cameraBindings[name];
+ if (idx == null) continue;
+ cameraDict[name] = {
+ type: "opencv",
+ camera_index: idx,
+ width: dims.width,
+ height: dims.height,
+ fps: DEFAULT_FPS,
+ };
+ }
+ try {
+ await startInference(baseUrl, fetchWithHeaders, {
+ follower_port: robot.follower_port,
+ follower_config: robot.follower_config,
+ policy_ref: selectedRef,
+ task,
+ cameras: cameraDict,
+ duration_s: durationS,
+ });
+ onOpenChange(false);
+ navigate("/inference");
+ } catch (e) {
+ toast({
+ title: "Couldn't start inference",
+ description: e instanceof Error ? e.message : String(e),
+ variant: "destructive",
+ });
+ // Failure: bring the previews back so the user can adjust.
+ setSubmitting(false);
+ }
+ };
+
+ const onCameraBindingChange = (name: string, value: string) => {
+ const idx = Number(value);
+ setCameraBindings((prev) => ({ ...prev, [name]: idx }));
+ };
+
+ return (
+
+
+
+
+
+ Configure Inference
+
+
+
+
+
+ Pick a checkpoint and confirm hardware. The selected policy will
+ drive the follower autonomously for the configured duration.
+
+
+
+
+ Robot Configuration
+
+ {!robot ? (
+
+
+
+ Select and configure a robot on the Landing page first.
+
+
+ ) : !robot.is_clean ? (
+
+
+
+ {robot.name} is missing a calibration.
+ Configure it before running inference.
+
+
+ ) : (
+
+
+
+ Running on {robot.name}
+
+
+ )}
+
+
+
+
+ Checkpoint
+
+ {checkpoints.length === 0 ? (
+
+
+
+ No checkpoints available for this job yet.
+
+
+ ) : (
+
+ )}
+
+
+
+
+ Run parameters
+
+ {policyConfig?.requires_task ? (
+
+
+ Task description
+
+
setTask(e.target.value)}
+ placeholder="e.g., pick up the red block"
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+ This policy is language-conditioned ({policyConfig.policy_type}).
+
+
+ ) : null}
+
+
+ Max duration (seconds)
+
+ {
+ if (v !== undefined) setDurationS(v);
+ }}
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+
+
+
+
+ Cameras
+
+ {policyConfigLoading ? (
+
+
+ Reading policy config…
+
+ ) : policyConfigError ? (
+
+
+
+ Couldn't load policy config: {policyConfigError}
+
+
+ ) : !policyConfig ? null : expectedCameraNames.length === 0 ? (
+
+ This policy doesn't use cameras.
+
+ ) : (
+
+
+ Bind a physical camera to each name the policy was trained
+ with. Resolution comes from the checkpoint.
+
+ {expectedCameraNames.map((name) => {
+ const dims = policyConfig.image_features[name];
+ const value = cameraBindings[name];
+ const bound =
+ value != null
+ ? availableCameras.find((c) => c.index === value)
+ : undefined;
+ return (
+
+
+
+ {name}
+
+
+ {dims.width}×{dims.height}
+
+
+
onCameraBindingChange(name, v)}
+ >
+
+
+
+
+ {availableCameras.length === 0 ? (
+
+ No cameras detected
+
+ ) : (
+ availableCameras.map((cam) => (
+
+ #{cam.index} — {cam.name}
+
+ ))
+ )}
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+ {submitting ? "Starting…" : "Start Inference"}
+
+
onOpenChange(false)}
+ variant="outline"
+ className="w-full sm:w-auto border-gray-500 hover:border-gray-200 px-10 py-6 text-lg text-zinc-500 bg-zinc-900 hover:bg-zinc-800"
+ >
+ Cancel
+
+
+
+
+
+ );
+};
+
+export default InferenceModal;
diff --git a/src/components/landing/LandingTopBar.tsx b/src/components/landing/LandingTopBar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c8646927aa50e948139ca7431e9a22baa282bf07
--- /dev/null
+++ b/src/components/landing/LandingTopBar.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import HfAuthChip from "./HfAuthChip";
+
+const LandingTopBar: React.FC = () => {
+ return (
+
+
+
+
+
+ LeLab
+
+
+
+
+
+ );
+};
+
+export default LandingTopBar;
diff --git a/src/components/landing/RecordingModal.tsx b/src/components/landing/RecordingModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c6d4d8ccbb7d25436f5e065b15ee3ab66a55c2c0
--- /dev/null
+++ b/src/components/landing/RecordingModal.tsx
@@ -0,0 +1,300 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { NumberInput } from "@/components/ui/number-input";
+import { Label } from "@/components/ui/label";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from "@/components/ui/dialog";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { AlertTriangle, CheckCircle, ChevronDown } from "lucide-react";
+import CameraConfiguration, {
+ CameraConfig,
+} from "@/components/recording/CameraConfiguration";
+import { useHfAuth } from "@/contexts/HfAuthContext";
+import { RobotRecord } from "@/hooks/useRobots";
+
+interface RecordingModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ robot: RobotRecord | null;
+ datasetName: string;
+ setDatasetName: (value: string) => void;
+ singleTask: string;
+ setSingleTask: (value: string) => void;
+ numEpisodes: number;
+ setNumEpisodes: (value: number) => void;
+ episodeTimeS: number;
+ setEpisodeTimeS: (value: number) => void;
+ resetTimeS: number;
+ setResetTimeS: (value: number) => void;
+ streamingEncoding: boolean;
+ setStreamingEncoding: (value: boolean) => void;
+ cameras: CameraConfig[];
+ setCameras: (cameras: CameraConfig[]) => void;
+ onStart: () => void;
+ releaseStreamsRef?: React.MutableRefObject<(() => void) | null>;
+}
+
+const RecordingModal: React.FC = ({
+ open,
+ onOpenChange,
+ robot,
+ datasetName,
+ setDatasetName,
+ singleTask,
+ setSingleTask,
+ numEpisodes,
+ setNumEpisodes,
+ episodeTimeS,
+ setEpisodeTimeS,
+ resetTimeS,
+ setResetTimeS,
+ streamingEncoding,
+ setStreamingEncoding,
+ cameras,
+ setCameras,
+ onStart,
+ releaseStreamsRef,
+}) => {
+ const { auth } = useHfAuth();
+
+ const canStart = !!robot && robot.is_clean;
+
+ return (
+
+
+
+
+
+ Configure Recording
+
+
+
+
+ Pick a configured robot and dataset parameters for recording.
+
+
+
+
+
+ Robot Configuration
+
+ {!robot ? (
+
+
+
+ Select and configure a robot on the Landing page before
+ recording.
+
+
+ ) : !robot.is_clean ? (
+
+
+
+ {robot.name} is missing a calibration.
+ Configure it before recording.
+
+
+ ) : (
+
+
+
+ Recording with {robot.name}
+
+
+ )}
+
+
+
+
+ Dataset Configuration
+
+
+
+
+ Dataset Name *
+
+
+ setDatasetName(
+ e.target.value.replace(/[^A-Za-z0-9._-]/g, "_")
+ )
+ }
+ placeholder="my_dataset"
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+ Letters, numbers, . _{" "}
+ - only — other characters become{" "}
+ _.
+
+ {datasetName &&
+ (auth.status === "authenticated" ? (
+
+ Will be saved as{" "}
+
+ {auth.username}/{datasetName}
+
+
+ ) : auth.status === "unauthenticated" ? (
+
+ Log in to Hugging Face to set the repository owner.
+
+ ) : null)}
+
+
+
+ Task Description *
+
+ setSingleTask(e.target.value)}
+ placeholder="e.g., pick up the red block and place it on the blue square"
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+
+
+ Number of Episodes
+
+ {
+ if (v !== undefined) setNumEpisodes(v);
+ }}
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+
+
+
+ Episode duration (seconds)
+
+ {
+ if (v !== undefined) setEpisodeTimeS(v);
+ }}
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+
+
+ Reset duration (seconds)
+
+ {
+ if (v !== undefined) setResetTimeS(v);
+ }}
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ Advanced Parameters
+
+
+
+
+
+ setStreamingEncoding(value === true)
+ }
+ className="mt-0.5 border-gray-500 data-[state=checked]:bg-red-500 data-[state=checked]:border-red-500"
+ />
+
+
+ Streaming video encoding
+
+
+ Encodes frames in real time during capture so each
+ episode saves almost instantly. Uncheck to fall back to
+ the slower PNG-then-encode flow.
+
+
+
+
+
+
+
+
+
+ Start Recording
+
+ onOpenChange(false)}
+ variant="outline"
+ className="w-full sm:w-auto border-gray-500 hover:border-gray-200 px-10 py-6 text-lg text-zinc-500 bg-zinc-900 hover:bg-zinc-800"
+ >
+ Cancel
+
+
+
+
+
+ );
+};
+
+export default RecordingModal;
diff --git a/src/components/landing/RobotConfigManager.tsx b/src/components/landing/RobotConfigManager.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7eec99a8395df90fb8e0c5474040acebf5ba50c8
--- /dev/null
+++ b/src/components/landing/RobotConfigManager.tsx
@@ -0,0 +1,85 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { useApi } from "@/contexts/ApiContext";
+import { useToast } from "@/hooks/use-toast";
+import { RobotRecord } from "@/hooks/useRobots";
+import RobotTile from "./RobotTile";
+
+interface RobotConfigManagerProps {
+ selectedName: string | null;
+ selectedRecord: RobotRecord | null;
+ availableNames: string[];
+ isLoading: boolean;
+ selectRobot: (name: string) => void;
+ createRobot: (name: string) => Promise;
+ deleteRobot: (name: string) => Promise;
+}
+
+const RobotConfigManager: React.FC = ({
+ selectedName,
+ selectedRecord,
+ availableNames,
+ isLoading,
+ selectRobot,
+ createRobot,
+ deleteRobot,
+}) => {
+ const navigate = useNavigate();
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const { toast } = useToast();
+
+ const handleConfigure = (name: string) => {
+ navigate("/calibration", { state: { robot_name: name } });
+ };
+
+ const handleTeleop = async (robot: RobotRecord) => {
+ try {
+ const res = await fetchWithHeaders(`${baseUrl}/move-arm`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ leader_port: robot.leader_port,
+ follower_port: robot.follower_port,
+ leader_config: robot.leader_config,
+ follower_config: robot.follower_config,
+ }),
+ });
+ const data = await res.json();
+ if (res.ok) {
+ toast({
+ title: "Teleoperation Started",
+ description: data.message || `Started teleoperation for ${robot.name}.`,
+ });
+ navigate("/teleoperation");
+ } else {
+ toast({
+ title: "Error Starting Teleoperation",
+ description: data.message || "Failed to start.",
+ variant: "destructive",
+ });
+ }
+ } catch (e) {
+ toast({
+ title: "Connection Error",
+ description: "Could not connect to the backend server.",
+ variant: "destructive",
+ });
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default RobotConfigManager;
diff --git a/src/components/landing/RobotSelector.tsx b/src/components/landing/RobotSelector.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7bdf1e254c51b3cb4c3c11d630e0865f3aa4e40b
--- /dev/null
+++ b/src/components/landing/RobotSelector.tsx
@@ -0,0 +1,144 @@
+import React, { useState } from "react";
+import { Plus, Check, ChevronsUpDown } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import { cn } from "@/lib/utils";
+
+interface RobotSelectorProps {
+ selectedName: string | null;
+ availableNames: string[];
+ onSelect: (name: string) => void;
+ onCreateNew: (name: string) => Promise;
+ isLoading: boolean;
+}
+
+const RobotSelector: React.FC = ({
+ selectedName,
+ availableNames,
+ onSelect,
+ onCreateNew,
+ isLoading,
+}) => {
+ const [open, setOpen] = useState(false);
+ const [query, setQuery] = useState("");
+
+ const trimmed = query.trim();
+ const matchesExisting = availableNames.some(
+ (n) => n.toLowerCase() === trimmed.toLowerCase()
+ );
+ const canCreate = trimmed.length > 0 && !matchesExisting;
+
+ const createDisabled = !canCreate;
+ const createLabel = matchesExisting
+ ? "Already exists"
+ : trimmed === ""
+ ? "Create new robot…"
+ : `Create "${trimmed}"`;
+
+ const reset = () => {
+ setQuery("");
+ setOpen(false);
+ };
+
+ const handlePickExisting = (name: string) => {
+ onSelect(name);
+ reset();
+ };
+
+ const handleCreate = async () => {
+ if (!canCreate) return;
+ const ok = await onCreateNew(trimmed);
+ if (ok) reset();
+ };
+
+ return (
+
+
+
+
+ {isLoading
+ ? "Loading..."
+ : selectedName ?? "Select a robot or type a new name"}
+
+
+
+
+
+
+ {
+ if (e.key === "Enter" && canCreate) {
+ e.preventDefault();
+ handleCreate();
+ }
+ }}
+ className="text-white"
+ />
+
+ {availableNames.length === 0 && (
+
+ No robots yet. Type a name to create one.
+
+ )}
+ {availableNames.length > 0 && (
+
+ {availableNames.map((name) => (
+ handlePickExisting(name)}
+ className="text-white aria-selected:bg-gray-700"
+ >
+
+ {name}
+
+ ))}
+
+ )}
+
+
+
+ {createLabel}
+
+
+
+
+ );
+};
+
+export default RobotSelector;
diff --git a/src/components/landing/RobotTile.tsx b/src/components/landing/RobotTile.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f2ffa55f6d97135d54128dc04c35be842a48743d
--- /dev/null
+++ b/src/components/landing/RobotTile.tsx
@@ -0,0 +1,160 @@
+import React, { useState } from "react";
+import { Settings, Trash2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { RobotRecord } from "@/hooks/useRobots";
+import RobotSelector from "./RobotSelector";
+
+interface RobotTileProps {
+ robot: RobotRecord | null;
+ selectedName: string | null;
+ availableNames: string[];
+ isLoading: boolean;
+ onSelect: (name: string) => void;
+ onCreateNew: (name: string) => Promise;
+ onConfigure: (name: string) => void;
+ onTeleop: (robot: RobotRecord) => void;
+ onDelete: (name: string) => void;
+}
+
+const RobotTile: React.FC = ({
+ robot,
+ selectedName,
+ availableNames,
+ isLoading,
+ onSelect,
+ onCreateNew,
+ onConfigure,
+ onTeleop,
+ onDelete,
+}) => {
+ const [confirmDelete, setConfirmDelete] = useState(false);
+ const status = robot ? (robot.is_clean ? "Ready" : "Needs configuration") : null;
+ const teleopDisabled = !robot || !robot.is_clean;
+
+ return (
+
+
+
+
+
+ {status && (
+
+ {status}
+
+ )}
+ {robot && (
+
+
+
+ onConfigure(robot.name)}
+ aria-label="Configure"
+ >
+
+
+
+ Configure (calibrate)
+
+
+
+ setConfirmDelete(true)}
+ aria-label="Delete robot"
+ >
+
+
+
+ Delete robot config
+
+
+ )}
+
+
+ {robot && (
+
+
+
+ onTeleop(robot)}
+ disabled={teleopDisabled}
+ className={`w-full ${
+ teleopDisabled
+ ? "bg-red-500/30 hover:bg-red-500/30 text-red-200 cursor-not-allowed"
+ : "bg-yellow-500 hover:bg-yellow-600 text-white"
+ }`}
+ >
+ Teleoperation
+
+
+
+ {teleopDisabled && (
+ Configure the robot first.
+ )}
+
+ )}
+
+ {robot && (
+
+
+
+ Delete robot config?
+
+ This deletes the robot config file from disk. Calibration files
+ are not removed. This cannot be undone.
+
+
+
+ setConfirmDelete(false)}
+ >
+ Cancel
+
+ {
+ setConfirmDelete(false);
+ await onDelete(robot.name);
+ }}
+ >
+ Delete
+
+
+
+
+ )}
+
+ );
+};
+
+export default RobotTile;
diff --git a/src/components/landing/UsageInstructionsModal.tsx b/src/components/landing/UsageInstructionsModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..89355a0f6f75c03b2936c7f95b13215d4e0224d7
--- /dev/null
+++ b/src/components/landing/UsageInstructionsModal.tsx
@@ -0,0 +1,107 @@
+import React, { useState } from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Terminal, ExternalLink, Copy, Check } from "lucide-react";
+
+const ONE_LINER =
+ "uv tool install git+https://github.com/huggingface/leLab.git && lelab";
+const LOCAL_URL = "http://localhost:8000/";
+
+interface UsageInstructionsModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ dismissible?: boolean;
+}
+
+const UsageInstructionsModal: React.FC = ({
+ open,
+ onOpenChange,
+ dismissible = true,
+}) => {
+ const [copied, setCopied] = useState(false);
+
+ const blockClose = (e: Event) => {
+ if (!dismissible) e.preventDefault();
+ };
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(ONE_LINER);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ } catch (err) {
+ console.warn("Clipboard write failed:", err);
+ }
+ };
+
+ return (
+ undefined}
+ >
+
+
+
+
+ Get Started with LeLab
+
+
+ LeLab runs on your machine. Click the command to copy it, then paste
+ in a terminal:
+
+
+
+
+
+ {ONE_LINER}
+
+
+ {copied ? (
+ <>
+
+ Copied
+ >
+ ) : (
+ <>
+
+ Copy
+ >
+ )}
+
+
+
+ After running, your browser will open the local LeLab app.
+
+
+
+
+ Open LeLab
+
+
+
+
+
+ );
+};
+
+export default UsageInstructionsModal;
diff --git a/src/components/recording/CameraConfiguration.tsx b/src/components/recording/CameraConfiguration.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..75cf925fce2ebae84e0a543c9c1da5e4b295952e
--- /dev/null
+++ b/src/components/recording/CameraConfiguration.tsx
@@ -0,0 +1,382 @@
+import React, { useState, useEffect, useCallback } from "react";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Input } from "@/components/ui/input";
+import { NumberInput } from "@/components/ui/number-input";
+import { Camera, Plus, X, VideoOff } from "lucide-react";
+import { useToast } from "@/hooks/use-toast";
+import { useAvailableCameras } from "@/hooks/useAvailableCameras";
+import { useCameraStream } from "@/hooks/useCameraStream";
+
+export interface CameraConfig {
+ id: string;
+ name: string;
+ type: string;
+ camera_index?: number; // cv2 index — what the recorder opens
+ device_id: string; // Browser deviceId matched to the cv2 index by AVFoundation localizedName
+ width: number;
+ height: number;
+ fps?: number;
+}
+
+interface CameraConfigurationProps {
+ cameras: CameraConfig[];
+ onCamerasChange: (cameras: CameraConfig[]) => void;
+ releaseStreamsRef?: React.MutableRefObject<(() => void) | null>; // Ref to expose stream release function
+}
+
+const CameraConfiguration: React.FC = ({
+ cameras,
+ onCamerasChange,
+ releaseStreamsRef,
+}) => {
+ const { toast } = useToast();
+
+ const {
+ cameras: availableCameras,
+ isLoading: isLoadingCameras,
+ } = useAvailableCameras();
+ const [selectedCameraIndex, setSelectedCameraIndex] = useState("");
+ const [cameraName, setCameraName] = useState("");
+
+ // cv2's AVFoundation order is uniqueID-sorted, so plugging/unplugging a
+ // device between sessions shifts indices. The browser device_id stays
+ // stable per-origin, so use it to refresh each seeded camera's
+ // camera_index — otherwise the recorder opens the wrong physical device
+ // and the dropdown's "already added" check guards a stale index.
+ useEffect(() => {
+ if (availableCameras.length === 0 || cameras.length === 0) return;
+ let changed = false;
+ const refreshed = cameras.map((cam) => {
+ if (!cam.device_id) return cam;
+ const match = availableCameras.find((m) => m.deviceId === cam.device_id);
+ if (match && match.index !== cam.camera_index) {
+ changed = true;
+ return { ...cam, camera_index: match.index };
+ }
+ return cam;
+ });
+ if (changed) onCamerasChange(refreshed);
+ // We deliberately don't depend on `cameras`/`onCamerasChange` to avoid
+ // re-running every keystroke in the camera-name input — re-syncing only
+ // when the available-cameras list itself changes is sufficient.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [availableCameras]);
+
+ const addCamera = () => {
+ if (!selectedCameraIndex || !cameraName.trim()) {
+ toast({
+ title: "Missing Information",
+ description: "Please select a camera and provide a name.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ const cameraIndex = parseInt(selectedCameraIndex);
+ const selectedCamera = availableCameras.find(
+ (cam) => cam.index === cameraIndex
+ );
+
+ if (!selectedCamera) {
+ toast({
+ title: "Invalid Camera",
+ description: "Selected camera is not available.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ // Block duplicates by either cv2 index or browser deviceId — a stale
+ // camera_index in a seeded camera can otherwise let the same physical
+ // device sneak in under a different index.
+ const isDuplicate = cameras.some(
+ (cam) =>
+ cam.camera_index === selectedCamera.index ||
+ (selectedCamera.deviceId && cam.device_id === selectedCamera.deviceId),
+ );
+ if (isDuplicate) {
+ toast({
+ title: "Camera Already Added",
+ description: "This camera is already in the configuration.",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ const newCamera: CameraConfig = {
+ id: `camera_${Date.now()}`,
+ name: cameraName.trim(),
+ type: "opencv",
+ camera_index: selectedCamera.index,
+ device_id: selectedCamera.deviceId,
+ width: 640,
+ height: 480,
+ fps: 30,
+ };
+
+ onCamerasChange([...cameras, newCamera]);
+
+ setSelectedCameraIndex("");
+ setCameraName("");
+
+ toast({
+ title: "Camera Added",
+ description: `${newCamera.name} has been added to the configuration.`,
+ });
+ };
+
+ const removeCamera = (cameraId: string) => {
+ onCamerasChange(cameras.filter((cam) => cam.id !== cameraId));
+ toast({
+ title: "Camera Removed",
+ description: "Camera has been removed from the configuration.",
+ });
+ };
+
+ const updateCamera = (cameraId: string, updates: Partial) => {
+ onCamerasChange(
+ cameras.map((cam) =>
+ cam.id === cameraId ? { ...cam, ...updates } : cam
+ )
+ );
+ };
+
+ // When the recording session is starting, the parent calls
+ // releaseStreamsRef.current() to make every CameraPreview drop its browser
+ // stream so cv2.VideoCapture can grab the camera exclusively.
+ const [streamsPaused, setStreamsPaused] = useState(false);
+ const releaseAllCameraStreams = useCallback(() => {
+ setStreamsPaused(true);
+ }, []);
+
+ useEffect(() => {
+ if (releaseStreamsRef) {
+ releaseStreamsRef.current = releaseAllCameraStreams;
+ }
+ }, [releaseStreamsRef, releaseAllCameraStreams]);
+
+
+ return (
+
+
+ Camera Configuration
+
+
+ {/* Add Camera Section */}
+
+
Add Camera
+
+
+
+
+ Available Cameras
+
+
+
+
+
+
+ {availableCameras.map((camera) => {
+ const alreadyAdded = cameras.some(
+ (cam) =>
+ cam.camera_index === camera.index ||
+ (camera.deviceId && cam.device_id === camera.deviceId),
+ );
+ return (
+
+
+ {camera.name}
+
+ Index {camera.index}
+ {alreadyAdded && " · already added"}
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ Camera Name
+
+ setCameraName(e.target.value)}
+ placeholder="e.g., workspace_cam"
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+
+
+
+
+
+ {/* Configured Cameras */}
+ {cameras.length > 0 && (
+
+
+ Configured Cameras ({cameras.length})
+
+
+
+ {cameras.map((camera) => (
+ removeCamera(camera.id)}
+ onUpdate={(updates) => updateCamera(camera.id, updates)}
+ />
+ ))}
+
+
+ )}
+
+ {cameras.length === 0 && (
+
+
+
No cameras configured. Add a camera to get started.
+
+ )}
+
+ );
+};
+
+interface CameraPreviewProps {
+ camera: CameraConfig;
+ paused: boolean;
+ onRemove: () => void;
+ onUpdate: (updates: Partial) => void;
+}
+
+const CameraPreview: React.FC = ({
+ camera,
+ paused,
+ onRemove,
+ onUpdate,
+}) => {
+ const { videoRef, hasError: streamError } = useCameraStream(
+ camera.device_id,
+ paused
+ );
+ const showVideo = !paused && camera.device_id && !streamError;
+ return (
+
+
+ {showVideo ? (
+
+ ) : (
+
+
+
+ {paused
+ ? "Preview paused"
+ : camera.device_id
+ ? "Preview failed"
+ : "No browser match"}
+
+
+ )}
+
+
+ {/* Camera Info */}
+
+
+
{camera.name}
+
+
+
+
+
+
+
+
Resolution:
+
+ {
+ if (v !== undefined) onUpdate({ width: v });
+ }}
+ className="bg-gray-800 border-gray-700 text-white text-xs h-6 px-2 w-16"
+ min="320"
+ max="1920"
+ />
+ ×
+ {
+ if (v !== undefined) onUpdate({ height: v });
+ }}
+ className="bg-gray-800 border-gray-700 text-white text-xs h-6 px-2 w-16"
+ min="240"
+ max="1080"
+ />
+
+
+
+ FPS:
+ {
+ if (v !== undefined) onUpdate({ fps: v });
+ }}
+ className="bg-gray-800 border-gray-700 text-white text-xs h-6 px-2 w-16"
+ min="10"
+ max="60"
+ />
+
+
+
+
+ Type: {camera.type} | Device: {camera.device_id?.substring(0, 10)}...
+
+
+
+ );
+};
+
+export default CameraConfiguration;
diff --git a/src/components/replay/DatasetCombobox.tsx b/src/components/replay/DatasetCombobox.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..17bce81522891f708582e2aed59569d99aace81d
--- /dev/null
+++ b/src/components/replay/DatasetCombobox.tsx
@@ -0,0 +1,114 @@
+import React from "react";
+import { Check, ChevronsUpDown, Pencil } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
+import { cn } from "@/lib/utils";
+import { DatasetItem } from "@/lib/replayApi";
+
+interface Props {
+ datasets: DatasetItem[];
+ loading: boolean;
+ value: string | null;
+ onChange: (repoId: string | null) => void;
+}
+
+const REPO_ID_RE = /^[\w.\-]+\/[\w.\-]+$/;
+
+const DatasetCombobox: React.FC = ({ datasets, loading, value, onChange }) => {
+ const [open, setOpen] = React.useState(false);
+ const [customMode, setCustomMode] = React.useState(false);
+ const [customValue, setCustomValue] = React.useState("");
+
+ const submitCustom = () => {
+ const v = customValue.trim();
+ if (REPO_ID_RE.test(v)) {
+ onChange(v);
+ setCustomMode(false);
+ }
+ };
+
+ const localDatasets = datasets.filter((d) => d.source === "local" || d.source === "both");
+ const hubDatasets = datasets.filter((d) => d.source === "hub");
+
+ const renderItem = (d: DatasetItem) => (
+ { onChange(d.repo_id); setOpen(false); }}
+ className="text-white aria-selected:bg-gray-700"
+ >
+
+ {d.repo_id}
+ {d.source === "both" && on Hub }
+ {d.private && private }
+
+ );
+
+ if (customMode) {
+ return (
+
+ setCustomValue(e.target.value)}
+ onKeyDown={(e) => { if (e.key === "Enter") submitCustom(); }}
+ placeholder="org/dataset-name"
+ className="bg-gray-800 border-gray-600 text-white"
+ />
+
+ Use
+
+ setCustomMode(false)}>
+ Cancel
+
+
+ );
+ }
+
+ return (
+
+
+
+ {value ?? (loading ? "Loading datasets…" : "Select a dataset…")}
+
+
+
+
+
+
+
+ {loading ? "Loading…" : "No datasets."}
+ {localDatasets.length > 0 && (
+
+ {localDatasets.map(renderItem)}
+
+ )}
+ {hubDatasets.length > 0 && (
+
+ {hubDatasets.map(renderItem)}
+
+ )}
+
+ { setCustomMode(true); setOpen(false); }}
+ className="text-purple-300 aria-selected:bg-gray-700"
+ >
+
+ Use custom repo ID…
+
+
+
+
+
+
+ );
+};
+
+export default DatasetCombobox;
diff --git a/src/components/training/ConfigurationTab.tsx b/src/components/training/ConfigurationTab.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8106bb798079fcfee84d931d11370f030413ea89
--- /dev/null
+++ b/src/components/training/ConfigurationTab.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import EssentialsCard from './config/EssentialsCard';
+import AdvancedCard from './config/AdvancedCard';
+import TargetCard from './config/TargetCard';
+import { ConfigComponentProps } from './types';
+import { DatasetItem } from '@/lib/replayApi';
+import { RunnerFlavor } from '@/lib/jobsApi';
+
+interface ConfigurationTabProps extends ConfigComponentProps {
+ datasets: DatasetItem[];
+ datasetsLoading: boolean;
+ authenticated: boolean;
+ flavors: RunnerFlavor[];
+ hardwareLoading: boolean;
+}
+
+const ConfigurationTab: React.FC = ({
+ config,
+ updateConfig,
+ datasets,
+ datasetsLoading,
+ authenticated,
+ flavors,
+ hardwareLoading,
+}) => {
+ return (
+
+ );
+};
+
+export default ConfigurationTab;
diff --git a/src/components/training/InstallProgress.tsx b/src/components/training/InstallProgress.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..25080ee20659e2f5700aaf74911fc10037ed52be
--- /dev/null
+++ b/src/components/training/InstallProgress.tsx
@@ -0,0 +1,176 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { useToast } from "@/hooks/use-toast";
+import {
+ AlertTriangle,
+ CheckCircle2,
+ Copy,
+ Loader2,
+ XCircle,
+} from "lucide-react";
+import type { InstallState, LogEntry } from "@/hooks/useInstallExtra";
+
+interface InstallProgressProps {
+ state: InstallState;
+ error: string | null;
+ logs: LogEntry[];
+ logBoxRef: React.RefObject;
+ onInstall: () => void;
+ onRetry: () => void;
+
+ installHint: string;
+ packageName: string;
+ idleTitle: string;
+ idleDescription: React.ReactNode;
+ doneDescription: React.ReactNode;
+}
+
+export function installTitle(state: InstallState, idleTitle: string): string {
+ switch (state) {
+ case "done":
+ return "Install Complete";
+ case "error":
+ return "Install Failed";
+ case "installing":
+ return "Installing…";
+ default:
+ return idleTitle;
+ }
+}
+
+export function InstallTitleIcon({ state }: { state: InstallState }) {
+ if (state === "done") return ;
+ if (state === "error") return ;
+ if (state === "installing")
+ return ;
+ return ;
+}
+
+export const InstallProgress: React.FC = ({
+ state,
+ error,
+ logs,
+ logBoxRef,
+ onInstall,
+ onRetry,
+ installHint,
+ packageName,
+ idleDescription,
+ doneDescription,
+}) => {
+ const { toast } = useToast();
+
+ const handleCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(installHint);
+ toast({ title: "Copied", description: installHint });
+ } catch {
+ toast({
+ title: "Copy failed",
+ description: "Select the command and copy manually.",
+ variant: "destructive",
+ });
+ }
+ };
+
+ return (
+ <>
+ {state === "idle" && (
+ <>
+ {idleDescription}
+
+
+ {installHint}
+
+
+
+
+
+
+ Install Now
+
+ >
+ )}
+
+ {state === "installing" && (
+
+ Installing{" "}
+
+ {packageName}
+
+ . This usually takes about 10 seconds.
+
+ )}
+
+ {state === "done" && (
+ {doneDescription}
+ )}
+
+ {state === "error" && (
+ <>
+ {error || "Install failed."}
+
+ Try again
+
+ >
+ )}
+
+ {state === "error" && logs.length > 0 && (
+
+ {logs.map((log, idx) => (
+
{log.message}
+ ))}
+
+ )}
+ >
+ );
+};
+
+export const RestartInstructions: React.FC<{ purpose: string }> = ({
+ purpose,
+}) => (
+ <>
+
+ Install complete. Restart{" "}
+
+ lelab
+ {" "}
+ to enable {purpose}:
+
+
+
+ Press{" "}
+
+ Ctrl+C
+ {" "}
+ in the terminal running{" "}
+
+ lelab
+
+ .
+
+
+ Run{" "}
+
+ lelab
+ {" "}
+ again.
+
+
+ >
+);
diff --git a/src/components/training/TrainingExtraGate.tsx b/src/components/training/TrainingExtraGate.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..018cac0dfaae657a80a3c1334abf09720e7621af
--- /dev/null
+++ b/src/components/training/TrainingExtraGate.tsx
@@ -0,0 +1,56 @@
+import React from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { useInstallExtra } from "@/hooks/useInstallExtra";
+import {
+ InstallProgress,
+ InstallTitleIcon,
+ RestartInstructions,
+ installTitle,
+} from "./InstallProgress";
+
+interface Props {
+ installHint: string;
+}
+
+const TrainingExtraGate: React.FC = ({ installHint }) => {
+ const install = useInstallExtra("system/training-extra");
+
+ return (
+
+
+
+
+
+ {installTitle(install.state, "Training Extra Not Installed")}
+
+
+
+
+ Training requires the{" "}
+
+ accelerate
+ {" "}
+ package, which isn't installed in this environment. Install it
+ to enable the Training page.
+ >
+ }
+ doneDescription={ }
+ />
+
+
+
+ );
+};
+
+export default TrainingExtraGate;
diff --git a/src/components/training/TrainingHeader.tsx b/src/components/training/TrainingHeader.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..120eb43ef59bafae489bf82cb979ca0d884a172b
--- /dev/null
+++ b/src/components/training/TrainingHeader.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Button } from '@/components/ui/button';
+import { ArrowLeft } from 'lucide-react';
+import Logo from '@/components/Logo';
+
+const TrainingHeader: React.FC = () => {
+ const navigate = useNavigate();
+ return (
+
+
+
navigate('/')}
+ className="text-slate-400 hover:bg-slate-800 hover:text-white rounded-lg"
+ >
+
+
+
+
Training
+
+
+ );
+};
+
+export default TrainingHeader;
diff --git a/src/components/training/WandbInstallDialog.tsx b/src/components/training/WandbInstallDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3ead2f07cd67d3b4ddc7f93324183a98a33e93b9
--- /dev/null
+++ b/src/components/training/WandbInstallDialog.tsx
@@ -0,0 +1,68 @@
+import React from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { useInstallExtra } from "@/hooks/useInstallExtra";
+import {
+ InstallProgress,
+ InstallTitleIcon,
+ RestartInstructions,
+ installTitle,
+} from "./InstallProgress";
+
+interface Props {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ installHint: string;
+}
+
+const WandbInstallDialog: React.FC = ({ open, onOpenChange, installHint }) => {
+ const install = useInstallExtra("system/wandb-extra", open);
+
+ return (
+
+
+
+
+
+ {installTitle(install.state, "Weights & Biases Not Installed")}
+
+
+ Install the wandb package to enable W&B logging.
+
+
+
+
+
+ Enabling W&B logging requires the{" "}
+
+ wandb
+ {" "}
+ package, which isn't installed in this environment. Install it
+ to log this run to W&B.
+ >
+ }
+ doneDescription={ }
+ />
+
+
+
+ );
+};
+
+export default WandbInstallDialog;
diff --git a/src/components/training/config/AdvancedCard.tsx b/src/components/training/config/AdvancedCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0556303bf4773d1b65306cb6bf60712bfd2c0942
--- /dev/null
+++ b/src/components/training/config/AdvancedCard.tsx
@@ -0,0 +1,339 @@
+import React, { useState } from 'react';
+import { Card, CardContent, CardHeader } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { NumberInput } from '@/components/ui/number-input';
+import { Label } from '@/components/ui/label';
+import { Switch } from '@/components/ui/switch';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { Separator } from '@/components/ui/separator';
+import { ChevronDown, ChevronRight } from 'lucide-react';
+import { ConfigComponentProps } from '../types';
+
+const SectionHeading: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+ {children}
+
+);
+
+const AdvancedCard: React.FC = ({ config, updateConfig }) => {
+ const [expanded, setExpanded] = useState(false);
+
+ return (
+
+ setExpanded((v) => !v)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setExpanded((v) => !v);
+ }
+ }}
+ className="cursor-pointer select-none flex flex-row items-center justify-between"
+ >
+ Advanced
+
+ {expanded ? (
+
+ ) : (
+
+ )}
+ {expanded ? 'Hide' : 'Show'}
+
+
+
+ {expanded && (
+
+ {/* Policy */}
+
+ Policy
+
+
+
+ Device
+
+ updateConfig('policy_device', value)}
+ >
+
+
+
+
+ CUDA (GPU)
+ CPU
+ MPS (Apple Silicon)
+
+
+
+
+ updateConfig('policy_use_amp', checked)}
+ />
+
+ Use Automatic Mixed Precision
+
+
+
+
+
+
+
+ {/* Training */}
+
+ Training
+
+
+
+ Random Seed
+
+ updateConfig('seed', v)}
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+ Number of Workers
+
+ {
+ if (v !== undefined) updateConfig('num_workers', v);
+ }}
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+
+
+
+ {/* Optimizer */}
+
+ Optimizer
+
+
+ Optimizer
+
+ updateConfig('optimizer_type', value)}
+ >
+
+
+
+
+ Adam
+ AdamW
+ SGD
+ Multi Adam
+
+
+
+
+
+
+ Learning Rate
+
+ updateConfig('optimizer_lr', v)}
+ placeholder="Use policy default"
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+ Weight Decay
+
+ updateConfig('optimizer_weight_decay', v)}
+ placeholder="Use policy default"
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+ Gradient Clipping
+
+ updateConfig('optimizer_grad_clip_norm', v)}
+ placeholder="Use policy default"
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+
+
+
+ {/* Logging & Checkpointing */}
+
+ Logging & Checkpointing
+
+
+
+ Log Frequency
+
+ {
+ if (v !== undefined) updateConfig('log_freq', v);
+ }}
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+ Save Frequency
+
+ {
+ if (v !== undefined) updateConfig('save_freq', v);
+ }}
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+ updateConfig('save_checkpoint', checked)}
+ />
+
+ Save Checkpoints
+
+
+
+ updateConfig('resume', checked)}
+ />
+
+ Resume from Checkpoint
+
+
+
+
+ {config.wandb_enable && (
+ <>
+
+
+ >
+ )}
+
+ {!config.wandb_enable && }
+
+ {/* Misc */}
+
+ Misc
+
+
+ updateConfig('use_policy_training_preset', checked)
+ }
+ />
+
+ Use Policy Training Preset
+
+
+
+
+ )}
+
+ );
+};
+
+export default AdvancedCard;
diff --git a/src/components/training/config/EssentialsCard.tsx b/src/components/training/config/EssentialsCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..050fdec9d38f03fe260624b22652672fa014e368
--- /dev/null
+++ b/src/components/training/config/EssentialsCard.tsx
@@ -0,0 +1,172 @@
+import React, { useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { NumberInput } from '@/components/ui/number-input';
+import { Label } from '@/components/ui/label';
+import { Switch } from '@/components/ui/switch';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { ConfigComponentProps } from '../types';
+import DatasetCombobox from '@/components/replay/DatasetCombobox';
+import { DatasetItem } from '@/lib/replayApi';
+import WandbInstallDialog from '../WandbInstallDialog';
+import { useApi } from '@/contexts/ApiContext';
+
+interface EssentialsCardProps extends ConfigComponentProps {
+ datasets: DatasetItem[];
+ datasetsLoading: boolean;
+}
+
+const EssentialsCard: React.FC = ({ config, updateConfig, datasets, datasetsLoading }) => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const [wandbDialogOpen, setWandbDialogOpen] = useState(false);
+ const [wandbInstallHint, setWandbInstallHint] = useState('pip install wandb');
+
+ const handleWandbToggle = async (checked: boolean) => {
+ if (!checked) {
+ updateConfig('wandb_enable', false);
+ return;
+ }
+ // Check availability before flipping the switch on. If wandb isn't
+ // importable in this lelab process, surface the same install flow used
+ // for the training extra (accelerate) instead of letting the user start
+ // a run that will fail.
+ try {
+ const r = await fetchWithHeaders(`${baseUrl}/system/wandb-extra`);
+ const data: { available: boolean; install_hint: string } = await r.json();
+ if (data.available) {
+ updateConfig('wandb_enable', true);
+ } else {
+ setWandbInstallHint(data.install_hint);
+ setWandbDialogOpen(true);
+ }
+ } catch {
+ // Backend unreachable — let the user proceed; training start will
+ // surface the real error if wandb is genuinely missing.
+ updateConfig('wandb_enable', true);
+ }
+ };
+
+ return (
+
+
+ Run Configuration
+
+
+
+
Dataset Repository ID *
+
+ {
+ if (repoId) updateConfig('dataset_repo_id', repoId);
+ }}
+ />
+
+
+ HuggingFace Hub dataset repository ID
+
+
+
+
+
+
+ Policy
+
+ updateConfig('policy_type', value)}
+ >
+
+
+
+
+ ACT (Action Chunking Transformer)
+ Diffusion Policy
+ PI0
+ SmolVLA
+ TD-MPC
+ VQ-BeT
+ PI0 Fast
+ SAC
+ Reward Classifier
+
+
+
+
+
+
+ Training Steps
+
+ {
+ if (v !== undefined) updateConfig('steps', v);
+ }}
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+
+ Batch Size
+
+ {
+ if (v !== undefined) updateConfig('batch_size', v);
+ }}
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+
+
+ Enable Weights & Biases
+
+
+
+
+
+
+ {config.wandb_enable && (
+
+
+ W&B Project Name
+
+
+ updateConfig('wandb_project', e.target.value || undefined)
+ }
+ placeholder="my-robotics-project"
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+ )}
+
+
+ );
+};
+
+export default EssentialsCard;
diff --git a/src/components/training/config/TargetCard.tsx b/src/components/training/config/TargetCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9da59f0cb061616f79d9586b6ed32cfff9629076
--- /dev/null
+++ b/src/components/training/config/TargetCard.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { ConfigComponentProps } from "../types";
+import { RunnerFlavor } from "@/lib/jobsApi";
+
+interface TargetCardProps extends ConfigComponentProps {
+ authenticated: boolean;
+ flavors: RunnerFlavor[];
+ loading: boolean;
+}
+
+const formatHourly = (unitCostUsd: number, unitLabel: string): string => {
+ const hourly = unitLabel === "minute" ? unitCostUsd * 60 : unitCostUsd;
+ return `$${hourly.toFixed(2)}/hr`;
+};
+
+const formatFlavorLine = (f: RunnerFlavor): string => {
+ const accel = f.accelerator ? f.accelerator : f.cpu;
+ return `${f.pretty_name} · ${accel} · ${formatHourly(f.unit_cost_usd, f.unit_label)}`;
+};
+
+const TargetCard: React.FC = ({
+ config,
+ updateConfig,
+ authenticated,
+ flavors,
+ loading,
+}) => {
+ const target = config.target;
+ const value =
+ target.runner === "local" ? "local" : `hf:${target.flavor ?? ""}`;
+
+ const handleChange = (v: string) => {
+ if (v === "local") {
+ updateConfig("target", { runner: "local" });
+ } else if (v.startsWith("hf:")) {
+ const flavor = v.slice("hf:".length);
+ updateConfig("target", { runner: "hf_cloud", flavor });
+ }
+ };
+
+ return (
+
+
+ Compute target
+
+
+
+
Run training on
+
+
+
+
+
+ Local — your machine (free)
+ {flavors.map((f) => (
+
+ {formatFlavorLine(f)}
+ {!authenticated && (
+
+ log in to HF
+
+ )}
+
+ ))}
+
+
+
+ Cost shown is per running hour. Final policy uploads to your HF
+ account when training completes.
+
+
+
+
+ );
+};
+
+export default TargetCard;
diff --git a/src/components/training/monitoring/MonitoringStats.tsx b/src/components/training/monitoring/MonitoringStats.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d7a48b4b6b5de74bc91144a8e7783a9393ae06e6
--- /dev/null
+++ b/src/components/training/monitoring/MonitoringStats.tsx
@@ -0,0 +1,279 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { TrainingStatus } from '../types';
+import { CheckCircle, Activity, Clock } from 'lucide-react';
+import { useApi } from '@/contexts/ApiContext';
+import { getJobMetricsHistory } from '@/lib/jobsApi';
+import {
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+
+interface MonitoringStatsProps {
+ jobId: string;
+ trainingStatus: TrainingStatus;
+ getProgressPercentage: () => number;
+ formatTime: (seconds: number) => string;
+}
+
+interface LossPoint {
+ step: number;
+ loss: number;
+}
+
+interface LrPoint {
+ step: number;
+ lr: number;
+}
+
+const HISTORY_CAP = 2000;
+
+const MonitoringStats: React.FC = ({
+ jobId,
+ trainingStatus,
+ getProgressPercentage,
+ formatTime,
+}) => {
+ const [lossHistory, setLossHistory] = useState([]);
+ const [lrHistory, setLrHistory] = useState([]);
+ const lastStepRef = useRef(0);
+ const { baseUrl, fetchWithHeaders } = useApi();
+
+ // Seed the curves from the persisted log on mount (and when the active job
+ // changes). Without this, the chart starts empty on every page reload,
+ // after navigating away and back, or after a lelab restart re-attaches to
+ // a still-running job. Live-append continues from the last seeded step.
+ useEffect(() => {
+ let cancelled = false;
+ getJobMetricsHistory(baseUrl, fetchWithHeaders, jobId)
+ .then((points) => {
+ if (cancelled || points.length === 0) return;
+ const lossSeed: LossPoint[] = points
+ .filter((p) => p.loss != null)
+ .map((p) => ({ step: p.step, loss: p.loss as number }))
+ .slice(-HISTORY_CAP);
+ const lrSeed: LrPoint[] = points
+ .filter((p) => p.lr != null)
+ .map((p) => ({ step: p.step, lr: p.lr as number }))
+ .slice(-HISTORY_CAP);
+ setLossHistory(lossSeed);
+ setLrHistory(lrSeed);
+ // Pin lastStepRef to the last seeded step so the first live tick
+ // (whose step is >= the seed's last step) doesn't trigger the
+ // step-regressed reset in the live-append effect below.
+ const lastSeededStep = points[points.length - 1]?.step ?? 0;
+ lastStepRef.current = lastSeededStep;
+ })
+ .catch(() => {
+ // 404 or transient — fall through; live ticks will populate from empty.
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [baseUrl, fetchWithHeaders, jobId]);
+
+ // Append new metric points as they arrive; reset when a new run starts
+ // (current_step resets back to 0).
+ useEffect(() => {
+ const step = trainingStatus.current_step;
+ if (step < lastStepRef.current) {
+ setLossHistory([]);
+ setLrHistory([]);
+ }
+ lastStepRef.current = step;
+
+ if (step > 0 && trainingStatus.current_loss != null) {
+ const loss = trainingStatus.current_loss;
+ setLossHistory((prev) => {
+ const last = prev[prev.length - 1];
+ if (last && last.step === step) return prev;
+ return [...prev, { step, loss }].slice(-HISTORY_CAP);
+ });
+ }
+
+ if (step > 0 && trainingStatus.current_lr != null) {
+ const lr = trainingStatus.current_lr;
+ setLrHistory((prev) => {
+ const last = prev[prev.length - 1];
+ if (last && last.step === step) return prev;
+ return [...prev, { step, lr }].slice(-HISTORY_CAP);
+ });
+ }
+ }, [trainingStatus.current_step, trainingStatus.current_loss, trainingStatus.current_lr]);
+
+ const progress = getProgressPercentage();
+ // Until tqdm fires its first progress line, total_steps is 0 — show
+ // "Training starting…" instead of a misleading 0/0 0% reading.
+ const isStarting = trainingStatus.training_active && trainingStatus.total_steps === 0;
+ const stepLabel = isStarting
+ ? 'Training starting…'
+ : `${trainingStatus.current_step.toLocaleString()} / ${trainingStatus.total_steps.toLocaleString()}`;
+ const etaLabel =
+ trainingStatus.eta_seconds != null ? formatTime(trainingStatus.eta_seconds) : '—';
+
+ return (
+
+
+
+
+
+
+
+
Progress
+
{stepLabel}
+
+
+
+
+
+ ETA {etaLabel}
+
+
+
+
+
+
+ {isStarting ? 'warming up…' : `${progress.toFixed(1)}%`}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loss{' '}
+
+ ({trainingStatus.current_loss?.toFixed(4) ?? '—'})
+
+
+
+
+
+
+ {lossHistory.length === 0 ? (
+
+ Waiting for first metric tick…
+
+ ) : (
+
+
+
+
+ v.toFixed(4)}
+ />
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ Learning Rate{' '}
+
+ ({trainingStatus.current_lr?.toExponential(2) ?? '—'})
+
+
+
+
+
+
+ {lrHistory.length === 0 ? (
+
+ Waiting for first metric tick…
+
+ ) : (
+
+
+
+ v.toExponential(0)}
+ />
+ v.toExponential(2)}
+ />
+
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default MonitoringStats;
diff --git a/src/components/training/monitoring/TrainingLogs.tsx b/src/components/training/monitoring/TrainingLogs.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..39a24548102699afac868628e03d29aae9c4b906
--- /dev/null
+++ b/src/components/training/monitoring/TrainingLogs.tsx
@@ -0,0 +1,51 @@
+
+import React from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { FileText } from 'lucide-react';
+import { LogEntry } from '../types';
+
+interface TrainingLogsProps {
+ logs: LogEntry[];
+ logContainerRef: React.RefObject;
+}
+
+const TrainingLogs: React.FC = ({ logs, logContainerRef }) => {
+ return (
+
+
+
+
+
+
+ Training Logs
+
+
+
+
+ {logs.length === 0 ? (
+
+ No training logs yet. Start training to see output.
+
+ ) : (
+ logs.map((log, index) => (
+
+
+ {new Date(log.timestamp * 1000).toLocaleTimeString()}
+
+ {log.message}
+
+ ))
+ )}
+
+
+
+ );
+};
+
+export default TrainingLogs;
diff --git a/src/components/training/types.ts b/src/components/training/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0ee5f44fecc5d7e6ee25c96e2b1b53211628e00
--- /dev/null
+++ b/src/components/training/types.ts
@@ -0,0 +1,73 @@
+export interface TrainingConfig {
+ target: { runner: "local" | "hf_cloud"; flavor?: string };
+
+ // Dataset configuration
+ dataset_repo_id: string;
+
+ // Policy configuration
+ policy_type: string;
+
+ // Core training parameters
+ steps: number;
+ batch_size: number;
+ seed?: number;
+ num_workers: number;
+
+ // Logging and checkpointing
+ log_freq: number;
+ save_freq: number;
+ save_checkpoint: boolean;
+
+ // Output configuration
+ resume: boolean;
+
+ // Weights & Biases
+ wandb_enable: boolean;
+ wandb_project?: string;
+ wandb_entity?: string;
+ wandb_notes?: string;
+ wandb_mode?: string;
+ wandb_disable_artifact: boolean;
+
+ // Policy-specific parameters
+ policy_device?: string;
+ policy_use_amp: boolean;
+
+ // Optimizer parameters
+ optimizer_type?: string;
+ optimizer_lr?: number;
+ optimizer_weight_decay?: number;
+ optimizer_grad_clip_norm?: number;
+
+ // Advanced configuration
+ use_policy_training_preset: boolean;
+}
+
+export interface TrainingStatus {
+ training_active: boolean;
+ current_step: number;
+ total_steps: number;
+ current_loss?: number;
+ current_lr?: number;
+ grad_norm?: number;
+ epoch_time?: number;
+ eta_seconds?: number;
+ available_controls: {
+ stop_training: boolean;
+ pause_training: boolean;
+ resume_training: boolean;
+ };
+}
+
+export interface LogEntry {
+ timestamp: number;
+ message: string;
+}
+
+export interface ConfigComponentProps {
+ config: TrainingConfig;
+ updateConfig: (
+ key: T,
+ value: TrainingConfig[T]
+ ) => void;
+}
diff --git a/src/components/ui/PortDetectionButton.tsx b/src/components/ui/PortDetectionButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3cc2833c3c94f5f0405a4b6dbf3ac838aad1c166
--- /dev/null
+++ b/src/components/ui/PortDetectionButton.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+import { Search } from "lucide-react";
+
+interface PortDetectionButtonProps {
+ onClick: () => void;
+ robotType?: "leader" | "follower";
+ className?: string;
+}
+
+const PortDetectionButton: React.FC = ({
+ onClick,
+ robotType,
+ className = "",
+}) => {
+ return (
+
+
+ Find
+
+ );
+};
+
+export default PortDetectionButton;
diff --git a/src/components/ui/PortDetectionModal.tsx b/src/components/ui/PortDetectionModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a66d541bfa274f619fc97c1a6cb88bed7a23ab60
--- /dev/null
+++ b/src/components/ui/PortDetectionModal.tsx
@@ -0,0 +1,242 @@
+import React, { useEffect, useRef, useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from "@/components/ui/dialog";
+import { Loader2, CheckCircle, AlertCircle } from "lucide-react";
+import { useToast } from "@/hooks/use-toast";
+import { useApi } from "@/contexts/ApiContext";
+
+interface PortDetectionModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ robotType: "leader" | "follower";
+ onPortDetected: (port: string) => void;
+}
+
+const SUCCESS_HOLD_MS = 2000;
+
+const PortDetectionModal: React.FC = ({
+ open,
+ onOpenChange,
+ robotType,
+ onPortDetected,
+}) => {
+ const [step, setStep] = useState<"detecting" | "success" | "error">(
+ "detecting"
+ );
+ const [detectedPort, setDetectedPort] = useState("");
+ const [error, setError] = useState("");
+ const cancelledRef = useRef(false);
+ const abortRef = useRef(null);
+ const successTimerRef = useRef(null);
+ const { toast } = useToast();
+ const { baseUrl, fetchWithHeaders } = useApi();
+
+ const runDetection = async () => {
+ try {
+ abortRef.current = new AbortController();
+ const startResponse = await fetchWithHeaders(
+ `${baseUrl}/start-port-detection`,
+ {
+ method: "POST",
+ body: JSON.stringify({ robot_type: robotType }),
+ signal: abortRef.current.signal,
+ }
+ );
+ const startData = await startResponse.json();
+ if (cancelledRef.current) return;
+ if (startData.status !== "success") {
+ throw new Error(startData.message || "Failed to start port detection");
+ }
+ const portsBefore: string[] = startData.data.ports_before;
+
+ // Poll the backend in a loop. Each call waits up to 15s for an unplug;
+ // we silently retry on timeout so the user has unlimited time to read
+ // and act. The loop ends on success, abort, or a non-timeout failure.
+ while (!cancelledRef.current) {
+ abortRef.current = new AbortController();
+ const response = await fetchWithHeaders(
+ `${baseUrl}/detect-port-after-disconnect`,
+ {
+ method: "POST",
+ body: JSON.stringify({ ports_before: portsBefore }),
+ signal: abortRef.current.signal,
+ }
+ );
+ const data = await response.json();
+ if (cancelledRef.current) return;
+
+ if (data.status === "success") {
+ setDetectedPort(data.port);
+ await savePort(data.port);
+ if (cancelledRef.current) return;
+ setStep("success");
+ toast({
+ title: "Port Detected Successfully",
+ description: `${robotType} port detected: ${data.port}`,
+ });
+ successTimerRef.current = window.setTimeout(() => {
+ if (cancelledRef.current) return;
+ onPortDetected(data.port);
+ onOpenChange(false);
+ }, SUCCESS_HOLD_MS);
+ return;
+ }
+
+ const message =
+ typeof data.message === "string" ? data.message : "";
+ if (message.includes("Timed out")) continue;
+ throw new Error(message || "Failed to detect port");
+ }
+ } catch (e) {
+ if (cancelledRef.current) return;
+ if (e instanceof DOMException && e.name === "AbortError") return;
+ console.error("Port detection failed:", e);
+ setError(e instanceof Error ? e.message : "Unknown error");
+ setStep("error");
+ }
+ };
+
+ const savePort = async (port: string) => {
+ try {
+ await fetchWithHeaders(`${baseUrl}/save-robot-port`, {
+ method: "POST",
+ body: JSON.stringify({ robot_type: robotType, port }),
+ });
+ } catch (e) {
+ console.error("Error saving port:", e);
+ }
+ };
+
+ useEffect(() => {
+ if (!open) return;
+ cancelledRef.current = false;
+ setStep("detecting");
+ setError("");
+ setDetectedPort("");
+ runDetection();
+ return () => {
+ cancelledRef.current = true;
+ abortRef.current?.abort();
+ if (successTimerRef.current !== null) {
+ window.clearTimeout(successTimerRef.current);
+ successTimerRef.current = null;
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open]);
+
+ const handleCancel = () => {
+ onOpenChange(false);
+ };
+
+ const handleRetry = () => {
+ cancelledRef.current = false;
+ abortRef.current?.abort();
+ setStep("detecting");
+ setError("");
+ setDetectedPort("");
+ runDetection();
+ };
+
+ const renderStepContent = () => {
+ switch (step) {
+ case "detecting":
+ return (
+
+
+
+
+ Unplug the {robotType} arm
+
+
+ Disconnect the {robotType} robot arm from USB. The port will be
+ detected automatically.
+
+
+
+
+ Cancel
+
+
+
+ );
+
+ case "success":
+ return (
+
+
+
+
+ Port Detected
+
+
+ {detectedPort}
+
+
+
+ );
+
+ case "error":
+ return (
+
+
+
+
+ Detection Failed
+
+
+
+
+
+ Try Again
+
+
+ Cancel
+
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+
+
+ Port Detection
+
+
+ Detect the USB port for your {robotType} arm
+
+
+
+ {renderStepContent()}
+
+
+ );
+};
+
+export default PortDetectionModal;
diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8722561cf6bda62d62f9a0c67730aefda971873a
--- /dev/null
+++ b/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..41fa7e0561a3fdb5f986c1213a35e563de740e96
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f000e3ef5176395b067dfc3f3e1256a80c450015
--- /dev/null
+++ b/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..36496a28727a3643b4212a14225d4f6cbd50bda5
--- /dev/null
+++ b/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..afa13ecfa3bd0f4a553a510b856c5800382e139b
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ddbdd01d8d1491ab772790db8d40c5ac0a2630f3
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,28 @@
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a23e7a281287e18b1c332498491b6bcc8d8e2b70
--- /dev/null
+++ b/src/components/ui/collapsible.tsx
@@ -0,0 +1,9 @@
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+const Collapsible = CollapsiblePrimitive.Root
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a6047d380c6df25c728da69343e10c4fb9736e75
--- /dev/null
+++ b/src/components/ui/command.tsx
@@ -0,0 +1,153 @@
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2490decf681d8abb8ef9f528e255bd2cc30f64b4
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -0,0 +1,124 @@
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ hideClose?: boolean
+ }
+>(({ className, children, hideClose, ...props }, ref) => (
+
+
+
+ {children}
+ {!hideClose && (
+
+
+ Close
+
+ )}
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..769ff7aa709f0d7a1afe2a87d180447fc26749e4
--- /dev/null
+++ b/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,198 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..68551b9276b4164a8263aa58d385db30f81a4453
--- /dev/null
+++ b/src/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..683faa793819982d64e21cb2939666fd6d4a7b13
--- /dev/null
+++ b/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/src/components/ui/number-input.tsx b/src/components/ui/number-input.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4b4a2b2a16ddd721599f2e704858a378147548c2
--- /dev/null
+++ b/src/components/ui/number-input.tsx
@@ -0,0 +1,60 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+import { Input } from "./input";
+
+type NumberInputProps = Omit<
+ React.ComponentProps,
+ "type" | "value" | "onChange"
+> & {
+ value: number | undefined | null;
+ onChange: (value: number | undefined) => void;
+ integer?: boolean;
+};
+
+const NumberInput = React.forwardRef(
+ ({ value, onChange, integer = true, className, ...props }, ref) => {
+ const [display, setDisplay] = React.useState(
+ value == null ? "" : String(value)
+ );
+ // Track the last value we saw from props so we only resync the
+ // visible string when the prop changes externally. This lets the
+ // user clear the field even when the parent keeps the previous
+ // numeric value (because our onChange(undefined) was ignored).
+ const lastPropRef = React.useRef(value);
+
+ React.useEffect(() => {
+ if (value !== lastPropRef.current) {
+ lastPropRef.current = value;
+ setDisplay(value == null ? "" : String(value));
+ }
+ }, [value]);
+
+ return (
+ {
+ const next = e.target.value;
+ setDisplay(next);
+ if (next === "") {
+ onChange(undefined);
+ return;
+ }
+ const n = integer ? parseInt(next, 10) : parseFloat(next);
+ if (Number.isFinite(n)) onChange(n);
+ }}
+ className={cn(
+ "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:m-0 [&::-webkit-outer-spin-button]:m-0",
+ className
+ )}
+ {...props}
+ />
+ );
+ }
+);
+NumberInput.displayName = "NumberInput";
+
+export { NumberInput };
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bbba7e0ebf26c29526552f2dc4e8e3ecea3d641a
--- /dev/null
+++ b/src/components/ui/popover.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent }
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dee7b1c6ee4f276986ef4c4028b1bb3ce549b742
--- /dev/null
+++ b/src/components/ui/select.tsx
@@ -0,0 +1,158 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6d7f12265ba0338704f013930ce4d52c56527dd1
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..35418149c0656c2257baf517e85496ef759677f5
--- /dev/null
+++ b/src/components/ui/sonner.tsx
@@ -0,0 +1,29 @@
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, toast } from "sonner"
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ )
+}
+
+export { Toaster, toast }
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..aa58baa29c676db7b14a37436a9662107b0111ec
--- /dev/null
+++ b/src/components/ui/switch.tsx
@@ -0,0 +1,27 @@
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a822477534192c4df5073e4015f7461e739d3344
--- /dev/null
+++ b/src/components/ui/toast.tsx
@@ -0,0 +1,127 @@
+import * as React from "react"
+import * as ToastPrimitives from "@radix-ui/react-toast"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const ToastProvider = ToastPrimitives.Provider
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ )
+})
+Toast.displayName = ToastPrimitives.Root.displayName
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastAction.displayName = ToastPrimitives.Action.displayName
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ToastClose.displayName = ToastPrimitives.Close.displayName
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastTitle.displayName = ToastPrimitives.Title.displayName
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastDescription.displayName = ToastPrimitives.Description.displayName
+
+type ToastProps = React.ComponentPropsWithoutRef
+
+type ToastActionElement = React.ReactElement
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+}
diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6c67edff67a48c6ac1c46f25a6d468dd461e66cb
--- /dev/null
+++ b/src/components/ui/toaster.tsx
@@ -0,0 +1,33 @@
+import { useToast } from "@/hooks/use-toast"
+import {
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+} from "@/components/ui/toast"
+
+export function Toaster() {
+ const { toasts } = useToast()
+
+ return (
+
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+
+
+ {title && {title} }
+ {description && (
+ {description}
+ )}
+
+ {action}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e121f0aea0b32952ae54d7db8965a2c15168b13e
--- /dev/null
+++ b/src/components/ui/tooltip.tsx
@@ -0,0 +1,28 @@
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/src/contexts/ApiContext.tsx b/src/contexts/ApiContext.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..824f02a27e84452156838af1c9a0ba78fef8e331
--- /dev/null
+++ b/src/contexts/ApiContext.tsx
@@ -0,0 +1,64 @@
+import React, { createContext, useContext, ReactNode, useState, useCallback, useMemo } from "react";
+
+interface ApiContextType {
+ baseUrl: string;
+ wsBaseUrl: string;
+ fetchWithHeaders: (url: string, options?: RequestInit) => Promise;
+}
+
+const ApiContext = createContext(undefined);
+
+const STORAGE_KEY = "lelab.apiBaseUrl";
+const DEFAULT_LOCALHOST = "http://localhost:8000";
+
+const httpToWs = (url: string): string => url.replace(/^http(s?):/, "ws$1:");
+
+const resolveInitialBaseUrl = (): string => {
+ if (typeof window === "undefined") return DEFAULT_LOCALHOST;
+
+ const fromQuery = new URLSearchParams(window.location.search).get("api");
+ if (fromQuery) {
+ try {
+ new URL(fromQuery);
+ const clean = fromQuery.replace(/\/$/, "");
+ window.localStorage.setItem(STORAGE_KEY, clean);
+ return clean;
+ } catch {
+ console.warn("Invalid `api` query param, ignoring:", fromQuery);
+ }
+ }
+
+ return window.localStorage.getItem(STORAGE_KEY) || DEFAULT_LOCALHOST;
+};
+
+export const ApiProvider: React.FC<{ children: ReactNode }> = ({
+ children,
+}) => {
+ const [baseUrl] = useState(resolveInitialBaseUrl);
+ const wsBaseUrl = httpToWs(baseUrl);
+
+ const fetchWithHeaders = useCallback(async (url: string, options: RequestInit = {}): Promise => {
+ return fetch(url, {
+ ...options,
+ headers: {
+ "Content-Type": "application/json",
+ ...options.headers,
+ },
+ });
+ }, []);
+
+ const value = useMemo(
+ () => ({ baseUrl, wsBaseUrl, fetchWithHeaders }),
+ [baseUrl, wsBaseUrl, fetchWithHeaders]
+ );
+
+ return {children} ;
+};
+
+export const useApi = (): ApiContextType => {
+ const context = useContext(ApiContext);
+ if (context === undefined) {
+ throw new Error("useApi must be used within an ApiProvider");
+ }
+ return context;
+};
diff --git a/src/contexts/DragAndDropContext.tsx b/src/contexts/DragAndDropContext.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e47864869be5693c6ed04944a10618f9a794c2e2
--- /dev/null
+++ b/src/contexts/DragAndDropContext.tsx
@@ -0,0 +1,113 @@
+import React, {
+ createContext,
+ useState,
+ useEffect,
+ useMemo,
+ ReactNode,
+ useCallback,
+} from "react";
+
+import { processDroppedFiles } from "@/lib/UrdfDragAndDrop";
+import { useUrdf } from "@/hooks/useUrdf";
+
+export type DragAndDropContextType = {
+ isDragging: boolean;
+ setIsDragging: (isDragging: boolean) => void;
+ handleDrop: (e: DragEvent) => Promise;
+};
+
+export const DragAndDropContext = createContext<
+ DragAndDropContextType | undefined
+>(undefined);
+
+interface DragAndDropProviderProps {
+ children: ReactNode;
+}
+
+export const DragAndDropProvider: React.FC = ({
+ children,
+}) => {
+ const [isDragging, setIsDragging] = useState(false);
+
+ // Get the Urdf context
+ const { urdfProcessor, processUrdfFiles } = useUrdf();
+
+ const handleDragOver = (e: DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ const handleDragEnter = (e: DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsDragging(true);
+ };
+
+ const handleDragLeave = (e: DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Only set isDragging to false if we're leaving the document
+ // This prevents flickering when moving between elements
+ if (!e.relatedTarget || !(e.relatedTarget as Element).closest("html")) {
+ setIsDragging(false);
+ }
+ };
+
+ const handleDrop = useCallback(
+ async (e: DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsDragging(false);
+
+ if (!e.dataTransfer || !urdfProcessor) return;
+
+ try {
+ const { availableModels, files } = await processDroppedFiles(
+ e.dataTransfer,
+ urdfProcessor
+ );
+ await processUrdfFiles(files, availableModels);
+ } catch (error) {
+ console.error("Error in handleDrop:", error);
+ }
+ },
+ [urdfProcessor, processUrdfFiles]
+ );
+
+ // Set up global event listeners
+ useEffect(() => {
+ document.addEventListener("dragover", handleDragOver);
+ document.addEventListener("dragenter", handleDragEnter);
+ document.addEventListener("dragleave", handleDragLeave);
+ document.addEventListener("drop", handleDrop);
+
+ return () => {
+ document.removeEventListener("dragover", handleDragOver);
+ document.removeEventListener("dragenter", handleDragEnter);
+ document.removeEventListener("dragleave", handleDragLeave);
+ document.removeEventListener("drop", handleDrop);
+ };
+ }, [handleDrop]); // Re-register when handleDrop changes
+
+ const value = useMemo(
+ () => ({ isDragging, setIsDragging, handleDrop }),
+ [isDragging, handleDrop]
+ );
+
+ return (
+
+ {children}
+ {isDragging && (
+
+
+
Drop Urdf Files Here
+
+ Release to upload your robot model
+
+
+
+ )}
+
+ );
+};
diff --git a/src/contexts/HfAuthContext.tsx b/src/contexts/HfAuthContext.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..abc9fa0c7f43344829a45cf6b7101f0eec4beb3a
--- /dev/null
+++ b/src/contexts/HfAuthContext.tsx
@@ -0,0 +1,80 @@
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ ReactNode,
+} from "react";
+import { useApi } from "./ApiContext";
+
+export type HfAuthState =
+ | { status: "loading" }
+ | { status: "authenticated"; username: string; orgs: string[] }
+ | { status: "unauthenticated"; loginCommand: string };
+
+interface HfAuthValue {
+ auth: HfAuthState;
+ refetch: () => Promise;
+}
+
+const HfAuthContext = createContext(undefined);
+
+export const HfAuthProvider: React.FC<{ children: ReactNode }> = ({
+ children,
+}) => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const [auth, setAuth] = useState({ status: "loading" });
+
+ const fetchStatus = useCallback(async () => {
+ setAuth({ status: "loading" });
+ try {
+ const response = await fetchWithHeaders(`${baseUrl}/hf-auth-status`);
+ const data = await response.json();
+ if (data.authenticated) {
+ setAuth({
+ status: "authenticated",
+ username: data.username,
+ orgs: data.orgs ?? [],
+ });
+ } else {
+ setAuth({
+ status: "unauthenticated",
+ loginCommand: data.login_command ?? "hf auth login",
+ });
+ }
+ } catch (err) {
+ console.warn("HF auth status fetch failed:", err);
+ // Drop to a terminal state so consumers waiting on `status !== "loading"`
+ // don't hang forever when the backend is unreachable.
+ setAuth({
+ status: "unauthenticated",
+ loginCommand: "hf auth login",
+ });
+ }
+ }, [baseUrl, fetchWithHeaders]);
+
+ useEffect(() => {
+ fetchStatus();
+ }, [fetchStatus]);
+
+ const value = useMemo(
+ () => ({ auth, refetch: fetchStatus }),
+ [auth, fetchStatus]
+ );
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useHfAuth = (): HfAuthValue => {
+ const ctx = useContext(HfAuthContext);
+ if (ctx === undefined) {
+ throw new Error("useHfAuth must be used within an HfAuthProvider");
+ }
+ return ctx;
+};
diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5ed92f83f5603f7b4e7193953285f90d7fbcfecb
--- /dev/null
+++ b/src/contexts/ThemeContext.tsx
@@ -0,0 +1,69 @@
+
+import { createContext, ReactNode, useState, useEffect, useCallback, useMemo } from "react";
+
+export type Theme = "dark" | "light" | "system";
+
+export interface ThemeProviderState {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+}
+
+const initialState: ThemeProviderState = {
+ theme: "system",
+ setTheme: () => null,
+};
+
+export const ThemeProviderContext =
+ createContext(initialState);
+
+interface ThemeProviderProps {
+ children: ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+}
+
+export function ThemeProvider({
+ children,
+ defaultTheme = "system",
+ storageKey = "vite-ui-theme",
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
+ );
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+ root.classList.remove("light", "dark");
+
+ if (theme === "system") {
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
+ .matches
+ ? "dark"
+ : "light";
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const updateTheme = useCallback(
+ (newTheme: Theme) => {
+ localStorage.setItem(storageKey, newTheme);
+ setTheme(newTheme);
+ },
+ [storageKey]
+ );
+
+ const value = useMemo(
+ () => ({ theme, setTheme: updateTheme }),
+ [theme, updateTheme]
+ );
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/contexts/UrdfContext.tsx b/src/contexts/UrdfContext.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ed3a0297d5adbca7c05b5d9e72eeb6c561d2eb15
--- /dev/null
+++ b/src/contexts/UrdfContext.tsx
@@ -0,0 +1,461 @@
+import React, {
+ createContext,
+ useState,
+ useCallback,
+ useMemo,
+ ReactNode,
+ useRef,
+ useEffect,
+} from "react";
+import { toast } from "sonner";
+import { UrdfProcessor, readUrdfFileContent } from "@/lib/UrdfDragAndDrop";
+import { UrdfFileModel } from "@/lib/types";
+import { RobotAnimationConfig } from "@/lib/types";
+
+// Define the result interface for Urdf detection
+interface UrdfDetectionResult {
+ hasUrdf: boolean;
+ modelName?: string;
+}
+
+// Define the context type
+export type UrdfContextType = {
+ urdfProcessor: UrdfProcessor | null;
+ registerUrdfProcessor: (processor: UrdfProcessor) => void;
+ onUrdfDetected: (
+ callback: (result: UrdfDetectionResult) => void
+ ) => () => void;
+ processUrdfFiles: (
+ files: Record,
+ availableModels: string[]
+ ) => Promise;
+ urdfBlobUrls: Record;
+ alternativeUrdfModels: string[];
+ isSelectionModalOpen: boolean;
+ setIsSelectionModalOpen: (isOpen: boolean) => void;
+ urdfModelOptions: UrdfFileModel[];
+ selectUrdfModel: (model: UrdfFileModel) => void;
+
+ isDefaultModel: boolean;
+ setIsDefaultModel: (isDefault: boolean) => void;
+ resetToDefaultModel: () => void;
+ urdfContent: string | null;
+
+ currentAnimationConfig: RobotAnimationConfig | null;
+ setCurrentAnimationConfig: (config: RobotAnimationConfig | null) => void;
+};
+
+// Create the context
+export const UrdfContext = createContext(
+ undefined
+);
+
+// Props for the provider component
+interface UrdfProviderProps {
+ children: ReactNode;
+}
+
+export const UrdfProvider: React.FC = ({ children }) => {
+ // State for Urdf processor
+ const [urdfProcessor, setUrdfProcessor] = useState(
+ null
+ );
+
+ // State for blob URLs (replacing window.urdfBlobUrls)
+ const [urdfBlobUrls, setUrdfBlobUrls] = useState>({});
+
+ // State for alternative models (replacing window.alternativeUrdfModels)
+ const [alternativeUrdfModels, setAlternativeUrdfModels] = useState(
+ []
+ );
+
+ // State for the Urdf selection modal
+ const [isSelectionModalOpen, setIsSelectionModalOpen] = useState(false);
+ const [urdfModelOptions, setUrdfModelOptions] = useState([]);
+
+ const [isDefaultModel, setIsDefaultModel] = useState(true);
+ const [urdfContent, setUrdfContent] = useState(null);
+
+ const [currentAnimationConfig, setCurrentAnimationConfig] =
+ useState(null);
+
+ useEffect(() => {
+ if (!isDefaultModel || urdfContent) return;
+ let cancelled = false;
+ (async () => {
+ try {
+ const response = await fetch("/so-101-urdf/urdf/so101_new_calib.urdf");
+ if (!response.ok) {
+ throw new Error(`Failed to fetch default Urdf: ${response.statusText}`);
+ }
+ const content = await response.text();
+ if (!cancelled) setUrdfContent(content);
+ } catch (error) {
+ if (!cancelled) {
+ console.error("Error loading default Urdf content:", error);
+ }
+ }
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [isDefaultModel, urdfContent]);
+
+ // Reference for callbacks
+ const urdfCallbacksRef = useRef<((result: UrdfDetectionResult) => void)[]>(
+ []
+ );
+
+ const resetToDefaultModel = useCallback(() => {
+ setIsDefaultModel(true);
+ setUrdfContent(null);
+ setCurrentAnimationConfig(null);
+
+ toast.info("Switched to default model", {
+ description: "The default ARM100 robot model is now displayed.",
+ });
+ }, []);
+
+ // Register a callback for Urdf detection
+ const onUrdfDetected = useCallback(
+ (callback: (result: UrdfDetectionResult) => void) => {
+ urdfCallbacksRef.current.push(callback);
+
+ return () => {
+ urdfCallbacksRef.current = urdfCallbacksRef.current.filter(
+ (cb) => cb !== callback
+ );
+ };
+ },
+ []
+ );
+
+ // Register a Urdf processor
+ const registerUrdfProcessor = useCallback((processor: UrdfProcessor) => {
+ setUrdfProcessor(processor);
+ }, []);
+
+ const notifyUrdfCallbacks = useCallback(
+ (result: UrdfDetectionResult) => {
+ if (result.hasUrdf) {
+ setIsDefaultModel(false);
+ } else {
+ resetToDefaultModel();
+ }
+ urdfCallbacksRef.current.forEach((callback) => callback(result));
+ },
+ [resetToDefaultModel]
+ );
+
+ // Helper function to process the selected Urdf model
+ const processSelectedUrdf = useCallback(
+ async (model: UrdfFileModel) => {
+ if (!urdfProcessor) return;
+
+ // Find the file in our files record
+ const files = Object.values(urdfBlobUrls)
+ .filter((url) => url === model.blobUrl)
+ .map((url) => {
+ const path = Object.keys(urdfBlobUrls).find(
+ (key) => urdfBlobUrls[key] === url
+ );
+ return path ? { path, url } : null;
+ })
+ .filter((item) => item !== null);
+
+ if (files.length === 0) {
+ console.error("❌ Could not find file for selected Urdf model");
+ return;
+ }
+
+ // Show a toast notification that we're loading the Urdf
+ const loadingToast = toast.loading("Loading Urdf model...", {
+ description: "Preparing 3D visualization",
+ duration: 5000,
+ });
+
+ try {
+ // Get the file from our record
+ const filePath = files[0]?.path;
+ if (!filePath || !urdfBlobUrls[filePath]) {
+ throw new Error("File not found in records");
+ }
+
+ // Get the actual File object
+ const response = await fetch(model.blobUrl);
+ const blob = await response.blob();
+ const file = new File(
+ [blob],
+ filePath.split("/").pop() || "model.urdf",
+ {
+ type: "application/xml",
+ }
+ );
+
+ const urdfContent = await readUrdfFileContent(file);
+
+ setUrdfContent(urdfContent);
+
+ toast.dismiss(loadingToast);
+
+ setIsDefaultModel(false);
+
+ const modelDisplayName =
+ model.name || model.path.split("/").pop() || "Unknown";
+
+ toast.success("Urdf model loaded successfully", {
+ description: `Model: ${modelDisplayName}`,
+ duration: 3000,
+ });
+
+ notifyUrdfCallbacks({
+ hasUrdf: true,
+ modelName: modelDisplayName,
+ });
+ } catch (error) {
+ // Error case
+ console.error("❌ Error processing selected Urdf:", error);
+ toast.dismiss(loadingToast);
+ toast.error("Error loading Urdf", {
+ description: `Error: ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ duration: 3000,
+ });
+
+ // Keep showing the custom model even if loading failed
+ // No need to reset to default unless user explicitly chooses to
+ }
+ },
+ [urdfBlobUrls, urdfProcessor, notifyUrdfCallbacks]
+ );
+
+ // Function to handle selecting a Urdf model from the modal
+ const selectUrdfModel = useCallback(
+ (model: UrdfFileModel) => {
+ if (!urdfProcessor) {
+ console.error("❌ No Urdf processor available");
+ return;
+ }
+
+ setIsSelectionModalOpen(false);
+
+ const modelName =
+ model.name ||
+ model.path
+ .split("/")
+ .pop()
+ ?.replace(/\.urdf$/i, "") ||
+ "Unknown";
+
+ urdfProcessor.loadUrdf(model.blobUrl);
+
+ setIsDefaultModel(false);
+
+ toast.info(`Loading model: ${modelName}`, {
+ description: "Preparing 3D visualization",
+ duration: 2000,
+ });
+
+ notifyUrdfCallbacks({
+ hasUrdf: true,
+ modelName,
+ });
+
+ processSelectedUrdf(model);
+ },
+ [urdfProcessor, notifyUrdfCallbacks, processSelectedUrdf]
+ );
+
+ const processUrdfFiles = useCallback(
+ async (files: Record, availableModels: string[]) => {
+ Object.values(urdfBlobUrls).forEach(URL.revokeObjectURL);
+ setUrdfBlobUrls({});
+ setAlternativeUrdfModels([]);
+ setUrdfModelOptions([]);
+
+ try {
+ if (availableModels.length > 0 && urdfProcessor) {
+ const newUrdfBlobUrls: Record = {};
+ availableModels.forEach((path) => {
+ if (files[path]) {
+ newUrdfBlobUrls[path] = URL.createObjectURL(files[path]);
+ }
+ });
+ setUrdfBlobUrls(newUrdfBlobUrls);
+
+ setAlternativeUrdfModels(availableModels);
+
+ const modelOptions: UrdfFileModel[] = availableModels.map((path) => {
+ const fileName = path.split("/").pop() || "";
+ const modelName = fileName.replace(/\.urdf$/i, "");
+ return {
+ path,
+ blobUrl: newUrdfBlobUrls[path],
+ name: modelName,
+ };
+ });
+
+ setUrdfModelOptions(modelOptions);
+
+ if (availableModels.length === 1) {
+ const fileName = availableModels[0].split("/").pop() || "";
+ const modelName = fileName.replace(/\.urdf$/i, "");
+
+ const blobUrl = newUrdfBlobUrls[availableModels[0]];
+ if (blobUrl) {
+ urdfProcessor.loadUrdf(blobUrl);
+
+ setIsDefaultModel(false);
+
+ if (files[availableModels[0]]) {
+ const loadingToast = toast.loading("Loading Urdf model...", {
+ description: "Preparing 3D visualization",
+ duration: 5000,
+ });
+
+ try {
+ const urdfContent = await readUrdfFileContent(
+ files[availableModels[0]]
+ );
+
+ setUrdfContent(urdfContent);
+
+ toast.dismiss(loadingToast);
+
+ toast.success("Urdf model loaded successfully", {
+ description: `Model: ${modelName}`,
+ duration: 3000,
+ });
+
+ notifyUrdfCallbacks({
+ hasUrdf: true,
+ modelName,
+ });
+ } catch (loadError) {
+ console.error("Error loading Urdf:", loadError);
+ toast.dismiss(loadingToast);
+ toast.error("Error loading Urdf", {
+ description: `Error: ${
+ loadError instanceof Error
+ ? loadError.message
+ : String(loadError)
+ }`,
+ duration: 3000,
+ });
+
+ // Still notify callbacks without detailed data
+ notifyUrdfCallbacks({
+ hasUrdf: true,
+ modelName,
+ });
+ }
+ } else {
+ console.error(
+ "Could not find file for Urdf model:",
+ availableModels[0]
+ );
+
+ notifyUrdfCallbacks({
+ hasUrdf: true,
+ modelName,
+ });
+ }
+ } else {
+ console.warn(
+ `No blob URL found for ${availableModels[0]}, using path directly`
+ );
+ urdfProcessor.loadUrdf(availableModels[0]);
+
+ setIsDefaultModel(false);
+
+ notifyUrdfCallbacks({
+ hasUrdf: true,
+ modelName,
+ });
+ }
+ } else {
+ setIsSelectionModalOpen(true);
+
+ notifyUrdfCallbacks({
+ hasUrdf: true,
+ modelName: "Multiple models available",
+ });
+ }
+ } else {
+ notifyUrdfCallbacks({ hasUrdf: false });
+
+ resetToDefaultModel();
+
+ toast.error("No Urdf file found", {
+ description: "Please upload a folder containing a .urdf file.",
+ duration: 3000,
+ });
+ }
+ } catch (error) {
+ console.error("Error processing Urdf files:", error);
+ toast.error("Error processing files", {
+ description: `Error: ${
+ error instanceof Error ? error.message : String(error)
+ }`,
+ duration: 3000,
+ });
+
+ resetToDefaultModel();
+ }
+ },
+ [notifyUrdfCallbacks, urdfBlobUrls, urdfProcessor, resetToDefaultModel]
+ );
+
+ // Revoke blob URLs only on unmount; ref tracks the latest set so we
+ // don't re-revoke on every state change.
+ const blobUrlsRef = useRef(urdfBlobUrls);
+ blobUrlsRef.current = urdfBlobUrls;
+ useEffect(() => {
+ return () => {
+ Object.values(blobUrlsRef.current).forEach(URL.revokeObjectURL);
+ };
+ }, []);
+
+ const contextValue = useMemo(
+ () => ({
+ urdfProcessor,
+ registerUrdfProcessor,
+ onUrdfDetected,
+ processUrdfFiles,
+ urdfBlobUrls,
+ alternativeUrdfModels,
+ isSelectionModalOpen,
+ setIsSelectionModalOpen,
+ urdfModelOptions,
+ selectUrdfModel,
+
+ isDefaultModel,
+ setIsDefaultModel,
+ resetToDefaultModel,
+ urdfContent,
+
+ currentAnimationConfig,
+ setCurrentAnimationConfig,
+ }),
+ [
+ urdfProcessor,
+ registerUrdfProcessor,
+ onUrdfDetected,
+ processUrdfFiles,
+ urdfBlobUrls,
+ alternativeUrdfModels,
+ isSelectionModalOpen,
+ urdfModelOptions,
+ selectUrdfModel,
+ isDefaultModel,
+ resetToDefaultModel,
+ urdfContent,
+ currentAnimationConfig,
+ ]
+ );
+
+ return (
+ {children}
+ );
+};
diff --git a/src/hooks/use-toast.ts b/src/hooks/use-toast.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2c14125ac48b874740770da80d9e424daf1db58b
--- /dev/null
+++ b/src/hooks/use-toast.ts
@@ -0,0 +1,191 @@
+import * as React from "react"
+
+import type {
+ ToastActionElement,
+ ToastProps,
+} from "@/components/ui/toast"
+
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 1000000
+
+type ToasterToast = ToastProps & {
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const
+
+let count = 0
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
+ return count.toString()
+}
+
+type ActionType = typeof actionTypes
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+
+interface State {
+ toasts: ToasterToast[]
+}
+
+const toastTimeouts = new Map>()
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId)
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ })
+ }, TOAST_REMOVE_DELAY)
+
+ toastTimeouts.set(toastId, timeout)
+}
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ }
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ }
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId)
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id)
+ })
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t
+ ),
+ }
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ }
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ }
+ }
+}
+
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
+
+function toast({ ...props }: Toast) {
+ const id = genId()
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss()
+ },
+ },
+ })
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ }
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState)
+
+ React.useEffect(() => {
+ listeners.push(setState)
+ return () => {
+ const index = listeners.indexOf(setState)
+ if (index > -1) {
+ listeners.splice(index, 1)
+ }
+ }
+ }, [state])
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ }
+}
+
+export { useToast, toast }
diff --git a/src/hooks/useAvailableCameras.ts b/src/hooks/useAvailableCameras.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dfdff13d9ee7eff11d9912872adee5f6a2c39491
--- /dev/null
+++ b/src/hooks/useAvailableCameras.ts
@@ -0,0 +1,102 @@
+import { useCallback, useEffect, useState } from "react";
+import { useApi } from "@/contexts/ApiContext";
+
+export interface AvailableCamera {
+ index: number;
+ name: string;
+ deviceId: string;
+ available: boolean;
+}
+
+const norm = (s: string) => s.toLowerCase().replace(/\s+/g, " ").trim();
+
+interface UseAvailableCamerasOptions {
+ /** When false, do nothing. Use to gate on modal open. */
+ enabled?: boolean;
+}
+
+/**
+ * Enumerates cv2 camera indices from `/available-cameras` and merges each
+ * with the matching browser deviceId (by AVFoundation localizedName) so
+ * callers can render a preview alongside the bound dropdowns. Refreshes on
+ * USB hotplug.
+ */
+export function useAvailableCameras({
+ enabled = true,
+}: UseAvailableCamerasOptions = {}) {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const [cameras, setCameras] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const refresh = useCallback(async (): Promise => {
+ setIsLoading(true);
+ try {
+ // Need a permission grant before enumerateDevices() returns labels.
+ try {
+ const probe = await navigator.mediaDevices.getUserMedia({ video: true });
+ probe.getTracks().forEach((t) => t.stop());
+ } catch {
+ // ignore — we'll still try to enumerate, just without labels
+ }
+
+ const browserDevices = (await navigator.mediaDevices.enumerateDevices())
+ .filter((d) => d.kind === "videoinput")
+ .map((d) => ({ deviceId: d.deviceId, label: d.label }));
+
+ const r = await fetchWithHeaders(`${baseUrl}/available-cameras`);
+ if (!r.ok) {
+ setCameras([]);
+ return [];
+ }
+ const data = await r.json();
+ const backendCams: {
+ index: number;
+ name?: string;
+ available: boolean;
+ }[] = data.cameras ?? [];
+
+ // Browser's MediaDeviceInfo.label starts with AVFoundation's localizedName
+ // but Chrome often appends "(vendorId:productId)". Match by exact, then
+ // prefix, then either-contains.
+ const used = new Set();
+ const merged: AvailableCamera[] = backendCams.map((cam) => {
+ const label = cam.name || `Camera ${cam.index}`;
+ const target = norm(label);
+ const candidates = browserDevices.filter(
+ (d) => !used.has(d.deviceId) && d.label
+ );
+ const match =
+ candidates.find((d) => norm(d.label) === target) ||
+ candidates.find((d) => norm(d.label).startsWith(target)) ||
+ candidates.find(
+ (d) => norm(d.label).includes(target) || target.includes(norm(d.label))
+ );
+ if (match) used.add(match.deviceId);
+ return {
+ index: cam.index,
+ name: label,
+ deviceId: match?.deviceId ?? "",
+ available: cam.available,
+ };
+ });
+ setCameras(merged);
+ return merged;
+ } catch {
+ setCameras([]);
+ return [];
+ } finally {
+ setIsLoading(false);
+ }
+ }, [baseUrl, fetchWithHeaders]);
+
+ useEffect(() => {
+ if (!enabled) return;
+ refresh();
+ const handler = () => refresh();
+ navigator.mediaDevices.addEventListener("devicechange", handler);
+ return () =>
+ navigator.mediaDevices.removeEventListener("devicechange", handler);
+ }, [enabled, refresh]);
+
+ return { cameras, isLoading, refresh };
+}
diff --git a/src/hooks/useCameraStream.ts b/src/hooks/useCameraStream.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f770fd25b15b497071c2af7441778b17f9df6973
--- /dev/null
+++ b/src/hooks/useCameraStream.ts
@@ -0,0 +1,46 @@
+import { useEffect, useRef, useState } from "react";
+
+/**
+ * Attach a live browser camera stream to a `` element by deviceId.
+ * Set `paused=true` to release the stream (e.g. so cv2.VideoCapture can claim
+ * the camera exclusively). The stream is auto-stopped on unmount.
+ */
+export function useCameraStream(deviceId: string, paused: boolean) {
+ const videoRef = useRef(null);
+ const [hasError, setHasError] = useState(false);
+
+ useEffect(() => {
+ if (paused || !deviceId) {
+ if (!deviceId) setHasError(true);
+ return;
+ }
+ let cancelled = false;
+ let stream: MediaStream | null = null;
+ setHasError(false);
+
+ (async () => {
+ try {
+ stream = await navigator.mediaDevices.getUserMedia({
+ video: { deviceId: { exact: deviceId } },
+ });
+ if (cancelled) {
+ stream.getTracks().forEach((t) => t.stop());
+ return;
+ }
+ if (videoRef.current) {
+ videoRef.current.srcObject = stream;
+ await videoRef.current.play().catch(() => {});
+ }
+ } catch {
+ setHasError(true);
+ }
+ })();
+
+ return () => {
+ cancelled = true;
+ if (stream) stream.getTracks().forEach((t) => t.stop());
+ };
+ }, [deviceId, paused]);
+
+ return { videoRef, hasError };
+}
diff --git a/src/hooks/useDatasets.ts b/src/hooks/useDatasets.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d0e6c0dbaf8aba4cfe97609901ea41b95c3ffcbf
--- /dev/null
+++ b/src/hooks/useDatasets.ts
@@ -0,0 +1,23 @@
+import { useCallback, useEffect, useState } from "react";
+import { useApi } from "@/contexts/ApiContext";
+import { DatasetItem, listDatasets } from "@/lib/replayApi";
+
+export const useDatasets = () => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const [datasets, setDatasets] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const refresh = useCallback(() => {
+ setLoading(true);
+ listDatasets(baseUrl, fetchWithHeaders)
+ .then(setDatasets)
+ .catch(() => setDatasets([]))
+ .finally(() => setLoading(false));
+ }, [baseUrl, fetchWithHeaders]);
+
+ useEffect(() => {
+ refresh();
+ }, [refresh]);
+
+ return { datasets, loading, refresh };
+};
diff --git a/src/hooks/useInstallExtra.ts b/src/hooks/useInstallExtra.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f53ba137b76d0f835772afed24987ba81296fdf4
--- /dev/null
+++ b/src/hooks/useInstallExtra.ts
@@ -0,0 +1,128 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { useApi } from "@/contexts/ApiContext";
+
+export type InstallState = "idle" | "installing" | "done" | "error";
+
+export interface LogEntry {
+ timestamp: number;
+ message: string;
+}
+
+interface InstallStatus {
+ state: InstallState;
+ error: string | null;
+ logs: LogEntry[];
+}
+
+const POLL_INTERVAL_MS = 1500;
+
+export interface UseInstallExtraResult {
+ state: InstallState;
+ error: string | null;
+ logs: LogEntry[];
+ logBoxRef: React.RefObject;
+ handleInstall: () => Promise;
+ handleRetry: () => void;
+}
+
+/**
+ * Drives the backend extra-install flow (`accelerate`, `wandb`, …). Seeds state
+ * from `${endpointPrefix}/install-status`, polls while installing, and exposes
+ * install/retry handlers. Pass `enabled=false` to gate seeding on dialog open.
+ */
+export function useInstallExtra(
+ endpointPrefix: string,
+ enabled: boolean = true
+): UseInstallExtraResult {
+ const { baseUrl, fetchWithHeaders } = useApi();
+
+ const [state, setState] = useState("idle");
+ const [error, setError] = useState(null);
+ const [logs, setLogs] = useState([]);
+ const logBoxRef = useRef(null);
+
+ // Seed local state from the backend so a refresh mid-install picks up where
+ // we left off (or shows Done/Error if the install already finished).
+ useEffect(() => {
+ if (!enabled) return;
+ let cancelled = false;
+ fetchWithHeaders(`${baseUrl}/${endpointPrefix}/install-status`)
+ .then((r) => r.json())
+ .then((status: InstallStatus) => {
+ if (cancelled) return;
+ setState(status.state);
+ setError(status.error);
+ if (status.logs.length > 0) setLogs(status.logs);
+ })
+ .catch(() => {
+ // Backend unreachable — stay in idle; the user can still try.
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [enabled, baseUrl, fetchWithHeaders, endpointPrefix]);
+
+ // Poll while installing.
+ useEffect(() => {
+ if (state !== "installing") return;
+ const id = setInterval(async () => {
+ try {
+ const r = await fetchWithHeaders(
+ `${baseUrl}/${endpointPrefix}/install-status`
+ );
+ if (!r.ok) return;
+ const status: InstallStatus = await r.json();
+ if (status.logs && status.logs.length > 0) {
+ setLogs((prev) => [...prev, ...status.logs]);
+ }
+ if (status.state !== "installing") {
+ setState(status.state);
+ setError(status.error);
+ }
+ } catch {
+ // Transient errors are fine; we'll retry on next tick.
+ }
+ }, POLL_INTERVAL_MS);
+ return () => clearInterval(id);
+ }, [state, baseUrl, fetchWithHeaders, endpointPrefix]);
+
+ // Auto-scroll the log panel as new lines arrive.
+ useEffect(() => {
+ if (logBoxRef.current) {
+ logBoxRef.current.scrollTop = logBoxRef.current.scrollHeight;
+ }
+ }, [logs]);
+
+ const handleInstall = useCallback(async () => {
+ setState("installing");
+ setError(null);
+ setLogs([]);
+ try {
+ const r = await fetchWithHeaders(
+ `${baseUrl}/${endpointPrefix}/install`,
+ { method: "POST" }
+ );
+ const body: { started: boolean; message: string } = await r.json();
+ if (!body.started && r.ok) return; // already installing
+ if (!r.ok) {
+ setState("error");
+ setError(body.message || `Install request failed (${r.status})`);
+ }
+ } catch (e) {
+ setState("error");
+ setError(
+ `Install request failed: ${
+ e instanceof Error ? e.message : String(e)
+ }`
+ );
+ }
+ }, [baseUrl, fetchWithHeaders, endpointPrefix]);
+
+ const handleRetry = useCallback(() => {
+ setState("idle");
+ setError(null);
+ setLogs([]);
+ }, []);
+
+ return { state, error, logs, logBoxRef, handleInstall, handleRetry };
+}
diff --git a/src/hooks/useJobsChangedSignal.ts b/src/hooks/useJobsChangedSignal.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b34b66e672f59b8b47f3a6e9b32b4954fb1b64f8
--- /dev/null
+++ b/src/hooks/useJobsChangedSignal.ts
@@ -0,0 +1,70 @@
+import { useEffect, useRef } from "react";
+import { useApi } from "@/contexts/ApiContext";
+import { JobProgressSnapshot } from "@/lib/jobsApi";
+
+/**
+ * Subscribe to backend job events on the shared /ws/joint-data channel.
+ *
+ * Two event types flow on this socket:
+ * - `jobs_changed` → fired on submit / watchdog finalisation / delete.
+ * Triggers `onChange` so the caller can refetch /jobs.
+ * - `job_progress` → fired by the watchdog (~1Hz) while jobs are running.
+ * Triggers `onProgress` with per-job snapshots so the
+ * UI can update the progress bar in place — no fetch.
+ *
+ * Callback refs are captured so identity changes don't tear down the socket.
+ * Auto-reconnects with a 3s delay if the server bounces.
+ */
+export const useJobsChangedSignal = (
+ onChange: () => void,
+ onProgress?: (snapshots: JobProgressSnapshot[]) => void,
+) => {
+ const { wsBaseUrl } = useApi();
+ const changeRef = useRef(onChange);
+ changeRef.current = onChange;
+ const progressRef = useRef(onProgress);
+ progressRef.current = onProgress;
+
+ useEffect(() => {
+ let cancelled = false;
+ let ws: WebSocket | null = null;
+ let reconnectTimer: ReturnType | null = null;
+
+ const connect = () => {
+ if (cancelled) return;
+ try {
+ ws = new WebSocket(`${wsBaseUrl}/ws/joint-data`);
+ } catch {
+ reconnectTimer = setTimeout(connect, 3000);
+ return;
+ }
+ ws.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ if (data?.type === "jobs_changed") {
+ changeRef.current();
+ } else if (
+ data?.type === "job_progress" &&
+ progressRef.current &&
+ Array.isArray(data?.jobs)
+ ) {
+ progressRef.current(data.jobs as JobProgressSnapshot[]);
+ }
+ } catch {
+ /* ignore non-JSON or unexpected payloads */
+ }
+ };
+ ws.onclose = () => {
+ if (cancelled) return;
+ reconnectTimer = setTimeout(connect, 3000);
+ };
+ };
+ connect();
+
+ return () => {
+ cancelled = true;
+ if (reconnectTimer) clearTimeout(reconnectTimer);
+ if (ws) ws.close();
+ };
+ }, [wsBaseUrl]);
+};
diff --git a/src/hooks/useRealTimeJoints.ts b/src/hooks/useRealTimeJoints.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0e74a69ebeff90580bcfe68a41e009441c7a2716
--- /dev/null
+++ b/src/hooks/useRealTimeJoints.ts
@@ -0,0 +1,130 @@
+import { useEffect, useRef, useState, useCallback } from "react";
+import { URDFViewerElement } from "@/lib/urdfViewerHelpers";
+import { useApi } from "@/contexts/ApiContext";
+
+interface JointData {
+ type: "joint_update";
+ joints: Record;
+ timestamp: number;
+}
+
+interface UseRealTimeJointsProps {
+ viewerRef: React.RefObject;
+ enabled?: boolean;
+ websocketUrl?: string;
+}
+
+const INITIAL_RECONNECT_DELAY_MS = 1000;
+const MAX_RECONNECT_DELAY_MS = 30000;
+
+export const useRealTimeJoints = ({
+ viewerRef,
+ enabled = true,
+ websocketUrl,
+}: UseRealTimeJointsProps) => {
+ const { wsBaseUrl } = useApi();
+ const finalWebSocketUrl = websocketUrl || `${wsBaseUrl}/ws/joint-data`;
+
+ const wsRef = useRef(null);
+ const reconnectTimeoutRef = useRef(null);
+ const reconnectDelayRef = useRef(INITIAL_RECONNECT_DELAY_MS);
+ const intentionallyClosedRef = useRef(false);
+ const [isConnected, setIsConnected] = useState(false);
+
+ const updateJointValues = useCallback(
+ (joints: Record) => {
+ const viewer = viewerRef.current;
+ if (!viewer || typeof viewer.setJointValue !== "function") return;
+ Object.entries(joints).forEach(([jointName, value]) => {
+ try {
+ viewer.setJointValue(jointName, value);
+ } catch (error) {
+ console.warn(`Failed to set joint ${jointName}:`, error);
+ }
+ });
+ },
+ [viewerRef]
+ );
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ intentionallyClosedRef.current = false;
+
+ const connect = () => {
+ if (intentionallyClosedRef.current) return;
+
+ let ws: WebSocket;
+ try {
+ ws = new WebSocket(finalWebSocketUrl);
+ } catch (error) {
+ console.error("Failed to create WebSocket:", error);
+ scheduleReconnect();
+ return;
+ }
+ wsRef.current = ws;
+
+ ws.onopen = () => {
+ setIsConnected(true);
+ reconnectDelayRef.current = INITIAL_RECONNECT_DELAY_MS;
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ reconnectTimeoutRef.current = null;
+ }
+ };
+
+ ws.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data) as JointData;
+ if (data.type === "joint_update" && data.joints) {
+ updateJointValues(data.joints);
+ }
+ } catch (error) {
+ console.error("Error parsing WebSocket message:", error);
+ }
+ };
+
+ ws.onclose = (event) => {
+ setIsConnected(false);
+ wsRef.current = null;
+ if (intentionallyClosedRef.current) return;
+ if (event.code === 1000) return; // clean close
+ scheduleReconnect();
+ };
+
+ ws.onerror = () => {
+ setIsConnected(false);
+ };
+ };
+
+ const scheduleReconnect = () => {
+ if (reconnectTimeoutRef.current) return;
+ const delay = reconnectDelayRef.current;
+ reconnectDelayRef.current = Math.min(
+ delay * 2,
+ MAX_RECONNECT_DELAY_MS
+ );
+ reconnectTimeoutRef.current = setTimeout(() => {
+ reconnectTimeoutRef.current = null;
+ connect();
+ }, delay);
+ };
+
+ connect();
+
+ return () => {
+ intentionallyClosedRef.current = true;
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ reconnectTimeoutRef.current = null;
+ }
+ if (wsRef.current) {
+ wsRef.current.close(1000);
+ wsRef.current = null;
+ }
+ setIsConnected(false);
+ };
+ }, [enabled, finalWebSocketUrl, updateJointValues]);
+
+ return { isConnected };
+};
diff --git a/src/hooks/useRobots.ts b/src/hooks/useRobots.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be5349221110d445173a1887ae5445b99c96fb5d
--- /dev/null
+++ b/src/hooks/useRobots.ts
@@ -0,0 +1,178 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useLocation } from "react-router-dom";
+import { useApi } from "@/contexts/ApiContext";
+import { useToast } from "@/hooks/use-toast";
+import type { CameraConfig } from "@/components/recording/CameraConfiguration";
+
+export interface RobotRecord {
+ name: string;
+ leader_port: string;
+ follower_port: string;
+ leader_config: string;
+ follower_config: string;
+ cameras: CameraConfig[];
+ is_clean: boolean;
+}
+
+const SELECTED_KEY = "lelab.selectedRobot";
+
+const readSelected = (): string | null => {
+ try {
+ const raw = localStorage.getItem(SELECTED_KEY);
+ return raw && typeof raw === "string" ? raw : null;
+ } catch {
+ return null;
+ }
+};
+
+const writeSelected = (name: string | null) => {
+ try {
+ if (name) localStorage.setItem(SELECTED_KEY, name);
+ else localStorage.removeItem(SELECTED_KEY);
+ } catch {
+ // Storage may be unavailable (private mode, quota). Failures here are non-fatal.
+ }
+};
+
+export const useRobots = () => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const { toast } = useToast();
+ const location = useLocation();
+
+ const [records, setRecords] = useState>({});
+ const [selectedName, setSelectedName] = useState(() => readSelected());
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Re-fetch records when location changes (RobotConfigManager mounts only on Landing,
+ // so this fires on initial mount and on back-navigation to Landing)
+ useEffect(() => {
+ let cancelled = false;
+ const fetchAll = async () => {
+ setIsLoading(true);
+ try {
+ const res = await fetchWithHeaders(`${baseUrl}/robots`);
+ const data = await res.json();
+ if (cancelled) return;
+ const next: Record = {};
+ for (const r of data.robots ?? []) next[r.name] = r;
+ setRecords(next);
+ // Drop the selection if the underlying record vanished (deleted from another tab)
+ setSelectedName((prev) => (prev && prev in next ? prev : null));
+ } catch (e) {
+ if (!cancelled) {
+ console.error("Failed to fetch robots:", e);
+ }
+ } finally {
+ if (!cancelled) setIsLoading(false);
+ }
+ };
+ fetchAll();
+ return () => {
+ cancelled = true;
+ };
+ }, [baseUrl, fetchWithHeaders, location.key]);
+
+ // Persist selection to localStorage
+ useEffect(() => {
+ writeSelected(selectedName);
+ }, [selectedName]);
+
+ const selectRobot = useCallback((name: string) => {
+ setSelectedName(name);
+ }, []);
+
+ const clearSelection = useCallback(() => {
+ setSelectedName(null);
+ }, []);
+
+ const createRobot = useCallback(
+ async (rawName: string): Promise => {
+ const name = rawName.trim();
+ if (!name) {
+ toast({ title: "Missing name", description: "Robot name cannot be empty.", variant: "destructive" });
+ return false;
+ }
+ if (/[/\\]|\.\./.test(name)) {
+ toast({ title: "Invalid name", description: "Robot names cannot contain '/', '\\', or '..'", variant: "destructive" });
+ return false;
+ }
+ try {
+ const res = await fetchWithHeaders(`${baseUrl}/robots/${encodeURIComponent(name)}?create=true`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: "{}",
+ });
+ if (res.status === 409) {
+ toast({
+ title: "Already exists",
+ description: `A robot named "${name}" already exists. Pick it from the dropdown or choose a different name.`,
+ variant: "destructive",
+ });
+ return false;
+ }
+ if (!res.ok) {
+ const text = await res.text();
+ toast({ title: "Failed to create", description: text, variant: "destructive" });
+ return false;
+ }
+ const data = await res.json();
+ if (data.robot) {
+ setRecords((prev) => ({ ...prev, [name]: data.robot }));
+ setSelectedName(name);
+ }
+ return true;
+ } catch (e) {
+ toast({ title: "Network error", description: String(e), variant: "destructive" });
+ return false;
+ }
+ },
+ [baseUrl, fetchWithHeaders, toast]
+ );
+
+ const deleteRobot = useCallback(
+ async (name: string): Promise => {
+ try {
+ const res = await fetchWithHeaders(`${baseUrl}/robots/${encodeURIComponent(name)}`, {
+ method: "DELETE",
+ });
+ if (!res.ok) {
+ const text = await res.text();
+ toast({ title: "Failed to delete", description: text, variant: "destructive" });
+ return false;
+ }
+ setRecords((prev) => {
+ const { [name]: _omit, ...rest } = prev;
+ return rest;
+ });
+ setSelectedName((prev) => (prev === name ? null : prev));
+ return true;
+ } catch (e) {
+ toast({ title: "Network error", description: String(e), variant: "destructive" });
+ return false;
+ }
+ },
+ [baseUrl, fetchWithHeaders, toast]
+ );
+
+ const selectedRecord = useMemo(
+ () => (selectedName ? records[selectedName] ?? null : null),
+ [selectedName, records]
+ );
+
+ const availableNames = useMemo(
+ () => Object.keys(records).sort(),
+ [records]
+ );
+
+ return {
+ records,
+ selectedName,
+ selectedRecord,
+ availableNames,
+ isLoading,
+ selectRobot,
+ clearSelection,
+ createRobot,
+ deleteRobot,
+ };
+};
diff --git a/src/hooks/useUrdf.ts b/src/hooks/useUrdf.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0c4b2f95c14ef3174debe7830ffe0c6a168539e2
--- /dev/null
+++ b/src/hooks/useUrdf.ts
@@ -0,0 +1,11 @@
+import { UrdfContextType, UrdfContext } from "@/contexts/UrdfContext";
+import { useContext } from "react";
+
+// Custom hook to use the Urdf context
+export const useUrdf = (): UrdfContextType => {
+ const context = useContext(UrdfContext);
+ if (context === undefined) {
+ throw new Error("useUrdf must be used within a UrdfProvider");
+ }
+ return context;
+};
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000000000000000000000000000000000000..53406e98832a88010d7edd10524428bbadb3839c
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,103 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Definition of the design system. All colors, gradients, fonts, etc should be defined here. */
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+
+ --radius: 0.5rem;
+
+ --sidebar-background: 0 0% 98%;
+
+ --sidebar-foreground: 240 5.3% 26.1%;
+
+ --sidebar-primary: 240 5.9% 10%;
+
+ --sidebar-primary-foreground: 0 0% 98%;
+
+ --sidebar-accent: 240 4.8% 95.9%;
+
+ --sidebar-accent-foreground: 240 5.9% 10%;
+
+ --sidebar-border: 220 13% 91%;
+
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/src/lib/UrdfDragAndDrop.ts b/src/lib/UrdfDragAndDrop.ts
new file mode 100644
index 0000000000000000000000000000000000000000..32323221b15d37b20a45d29ca82935e061fb757f
--- /dev/null
+++ b/src/lib/UrdfDragAndDrop.ts
@@ -0,0 +1,415 @@
+/**
+ * Urdf Drag and Drop Utility
+ *
+ * This file provides functionality for handling drag and drop of Urdf folders.
+ * It converts the dropped files into accessible blobs for visualization.
+ */
+
+/**
+ * Converts a DataTransfer structure into an object with all paths and files.
+ * @param dataTransfer The DataTransfer object from the drop event
+ * @returns A promise that resolves with the file structure object
+ */
+function dataTransferToFiles(
+ dataTransfer: DataTransfer
+): Promise> {
+ if (!(dataTransfer instanceof DataTransfer)) {
+ throw new Error('Data must be of type "DataTransfer"');
+ }
+
+ const files: Record = {};
+
+ /**
+ * Recursively processes a directory entry to extract all files
+ * Using type 'unknown' and then type checking for safety with WebKit's non-standard API
+ */
+ function recurseDirectory(item: unknown): Promise {
+ // Type guard for file entries
+ const isFileEntry = (
+ entry: unknown
+ ): entry is {
+ isFile: boolean;
+ fullPath: string;
+ file: (callback: (file: File) => void) => void;
+ } =>
+ entry !== null &&
+ typeof entry === "object" &&
+ "isFile" in entry &&
+ typeof (entry as Record).file === "function" &&
+ "fullPath" in entry;
+
+ // Type guard for directory entries
+ const isDirEntry = (
+ entry: unknown
+ ): entry is {
+ isFile: boolean;
+ createReader: () => {
+ readEntries: (callback: (entries: unknown[]) => void) => void;
+ };
+ } =>
+ entry !== null &&
+ typeof entry === "object" &&
+ "isFile" in entry &&
+ typeof (entry as Record).createReader === "function";
+
+ if (isFileEntry(item) && item.isFile) {
+ return new Promise((resolve) => {
+ item.file((file: File) => {
+ files[item.fullPath] = file;
+ resolve();
+ });
+ });
+ } else if (isDirEntry(item) && !item.isFile) {
+ const reader = item.createReader();
+
+ return new Promise((resolve) => {
+ const promises: Promise[] = [];
+
+ // Exhaustively read all directory entries
+ function readNextEntries() {
+ reader.readEntries((entries: unknown[]) => {
+ if (entries.length === 0) {
+ Promise.all(promises).then(() => resolve());
+ } else {
+ entries.forEach((entry) => {
+ promises.push(recurseDirectory(entry));
+ });
+ readNextEntries();
+ }
+ });
+ }
+
+ readNextEntries();
+ });
+ }
+
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve) => {
+ // Process dropped items
+ const dtitems = dataTransfer.items && Array.from(dataTransfer.items);
+ const dtfiles = Array.from(dataTransfer.files);
+
+ if (dtitems && dtitems.length && "webkitGetAsEntry" in dtitems[0]) {
+ const promises: Promise