diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..c1709971c22830fa68a19d55b41ffed5b5038dd1
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,42 @@
+*.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
+
+# Plain binary (overrides LFS): installing from git skips LFS, and the build copies these into the wheel.
+# sync_space.yml puts them back in LFS for the Space.
+public/so-101-urdf/meshes/*.stl -filter -diff -merge
+public/so-101-urdf/meshes/*.stl filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..7fef8a294454a94979716efc275a67bfe4368206
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
+# Dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# Testing
+/coverage
+
+# Production
+/build
+/dist
+
+# 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..25270727604c65be8fa277ecb398c35577133370
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,38 @@
+# Vite 8 requires Node ^20.19.0 or >=22.12.0.
+FROM node:22-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 straight from the committed lockfile. `npm ci` honours
+# the `resolved` URLs in package-lock.json and ignores the builder's configured
+# registry, so the install can't be derailed by a slow/incomplete npm mirror.
+RUN npm ci
+
+# 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..367e9e2bd463a02b82bf36568ec694f8a5692c82
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,8426 @@
+{
+ "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": "^7.18.3",
+ "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": "^4.3.1",
+ "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",
+ "jsdom": "^28.1.0",
+ "lovable-tagger": "^1.3.0",
+ "postcss": "^8.5.25",
+ "tailwindcss": "^3.4.11",
+ "typescript": "^5.5.3",
+ "typescript-eslint": "^8.0.1",
+ "vite": "^8.0.16",
+ "vitest": "^5.0.1"
+ }
+ },
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.31",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
+ "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz",
+ "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.1.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.2.6"
+ }
+ },
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz",
+ "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
+ "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz",
+ "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^6.1.1",
+ "@csstools/css-calc": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz",
+ "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.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/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "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.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "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.1",
+ "minimatch": "^3.1.5",
+ "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.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "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.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "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/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@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.4.0",
+ "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
+ "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
+ "license": "MIT",
+ "dependencies": {
+ "promise-worker-transferable": "^1.0.4"
+ },
+ "peerDependencies": {
+ "three": ">= 0.159.0"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "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/@oxc-project/types": {
+ "version": "0.133.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
+ "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
+ "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@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.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
+ "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@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-alert-dialog/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-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.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.8.tgz",
+ "integrity": "sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz",
+ "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.4",
+ "@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-avatar/node_modules/@radix-ui/react-context": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz",
+ "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@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.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
+ "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@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-collection/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-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.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
+ "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-menu": "2.1.16",
+ "@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.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@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-dialog/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-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
+ "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-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.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
+ "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/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-menubar": {
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz",
+ "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.2.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
+ "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@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.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-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-popover/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-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive/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-progress": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz",
+ "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz",
+ "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
+ "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-progress/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
+ "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@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.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@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.5",
+ "@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.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-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-select/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-separator": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
+ "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz",
+ "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+ "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz",
+ "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@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.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
+ "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@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.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
+ "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-toggle": "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-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-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-tooltip/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-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",
+ "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/@rolldown/binding-android-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
+ "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
+ "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
+ "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
+ "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
+ "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
+ "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
+ "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
+ "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
+ "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@swc/core": {
+ "version": "1.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.40.tgz",
+ "integrity": "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.26"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.15.40",
+ "@swc/core-darwin-x64": "1.15.40",
+ "@swc/core-linux-arm-gnueabihf": "1.15.40",
+ "@swc/core-linux-arm64-gnu": "1.15.40",
+ "@swc/core-linux-arm64-musl": "1.15.40",
+ "@swc/core-linux-ppc64-gnu": "1.15.40",
+ "@swc/core-linux-s390x-gnu": "1.15.40",
+ "@swc/core-linux-x64-gnu": "1.15.40",
+ "@swc/core-linux-x64-musl": "1.15.40",
+ "@swc/core-win32-arm64-msvc": "1.15.40",
+ "@swc/core-win32-ia32-msvc": "1.15.40",
+ "@swc/core-win32-x64-msvc": "1.15.40"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz",
+ "integrity": "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==",
+ "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.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz",
+ "integrity": "sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==",
+ "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.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz",
+ "integrity": "sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz",
+ "integrity": "sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==",
+ "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.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz",
+ "integrity": "sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-ppc64-gnu": {
+ "version": "1.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz",
+ "integrity": "sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-s390x-gnu": {
+ "version": "1.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz",
+ "integrity": "sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz",
+ "integrity": "sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==",
+ "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.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz",
+ "integrity": "sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==",
+ "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.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz",
+ "integrity": "sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==",
+ "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.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz",
+ "integrity": "sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==",
+ "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.15.40",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz",
+ "integrity": "sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==",
+ "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.26",
+ "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
+ "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
+ "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "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.101.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz",
+ "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.101.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz",
+ "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.101.0"
+ },
+ "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/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "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.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "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/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/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.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/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.19.19",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
+ "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
+ "dev": true,
+ "license": "MIT",
+ "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.30",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.30.tgz",
+ "integrity": "sha512-3ek6mwJL5/VBewBcY4S66cqlCtK3qi4WIq37Z0m/NHw1hjhI7274Mx1qz/+ggSzyBCOEf7eHjBN6INjPAWYfYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@types/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.184.1",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
+ "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~1.1.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
+ "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.60.1",
+ "@typescript-eslint/type-utils": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.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.60.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz",
+ "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.60.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
+ "debug": "^4.4.3"
+ },
+ "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 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz",
+ "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.60.1",
+ "@typescript-eslint/types": "^8.60.1",
+ "debug": "^4.4.3"
+ },
+ "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 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz",
+ "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1"
+ },
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz",
+ "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==",
+ "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 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz",
+ "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.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 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz",
+ "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==",
+ "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.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz",
+ "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.60.1",
+ "@typescript-eslint/tsconfig-utils": "8.60.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.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 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz",
+ "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.60.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1"
+ },
+ "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 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz",
+ "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.60.1",
+ "eslint-visitor-keys": "^5.0.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/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/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": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.1.tgz",
+ "integrity": "sha512-PaeokKjAGraNN+s5SIApgsktnJprIyt3zgEIu7awnEdfn29QiB2crTcCzyi2XGpX9rUnTc0cKU07Wm0N0g7H2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.0",
+ "@swc/core": "^1.15.11"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz",
+ "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.31",
+ "@vitest/spy": "5.0.1",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^1.2.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz",
+ "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "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-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/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/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "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.28.2",
+ "caniuse-lite": "^1.0.30001787",
+ "fraction.js": "^5.3.4",
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.21",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
+ "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "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.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "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.28.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
+ "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.11.20",
+ "caniuse-lite": "^1.0.30001810",
+ "electron-to-chromium": "^1.5.420",
+ "node-releases": "^2.0.54",
+ "update-browserslist-db": "^1.3.2"
+ },
+ "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.30001810",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/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/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/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/cssstyle": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz",
+ "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.0.1",
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.28",
+ "css-tree": "^3.1.0",
+ "lru-cache": "^11.2.6"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/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.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "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/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "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",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node-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/electron-to-chromium": {
+ "version": "1.5.422",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz",
+ "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==",
+ "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"
+ },
+ "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/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "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.5",
+ "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.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "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.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "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/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/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.4.0",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+ "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.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "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.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "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-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/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/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.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hls.js": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
+ "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/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.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "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-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-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/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.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "28.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz",
+ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@acemir/cssom": "^0.9.31",
+ "@asamuzakjp/dom-selector": "^6.8.1",
+ "@bramus/specificity": "^2.4.2",
+ "@exodus/bytes": "^1.11.0",
+ "cssstyle": "^6.0.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "parse5": "^8.0.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.0",
+ "undici": "^7.21.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "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/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/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.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "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.3.0",
+ "resolved": "https://registry.npmjs.org/lovable-tagger/-/lovable-tagger-1.3.0.tgz",
+ "integrity": "sha512-X2TX4g+/8VUFIBboGBjgXIdjEhR1ojtz9W65MO3g069DXTX5PPFZ4ihQzzociTL9HCwCC8SleVGT/wcfURhMXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "tailwindcss": "^3.4.17"
+ },
+ "peerDependencies": {
+ "vite": ">=5.0.0 <9.0.0"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/lovable-tagger/node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "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": "1.4.2",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
+ "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.6.0"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.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": "1.1.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
+ "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.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "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.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "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.54",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
+ "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/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/obug": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
+ "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "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/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/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "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/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.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/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.25",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
+ "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-nested/node_modules/postcss-selector-parser": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
+ "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
+ "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",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.2.tgz",
+ "integrity": "sha512-LK68OTbHB3oJNhl9cA0qVizzp3o26w61YSjAFkYi67N86iro32wx86kSNeFU/hq+gI8m1yzWhnomMLfZ041RzQ==",
+ "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 || ^19.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",
+ "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.77.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.77.0.tgz",
+ "integrity": "sha512-Sslh9YDYc0GDlWT/lxasnIduNo4v3yyvqRGvmGKUre5AFjDs/HV9/OafHGD8d+sB2yoL4UIL9L8X9i0WlZZebg==",
+ "license": "MIT",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "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": "7.18.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz",
+ "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz",
+ "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.18.3"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "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.4",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
+ "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
+ "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide",
+ "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.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "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/rolldown": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.133.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.3",
+ "@rolldown/binding-darwin-arm64": "1.0.3",
+ "@rolldown/binding-darwin-x64": "1.0.3",
+ "@rolldown/binding-freebsd-x64": "1.0.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
+ }
+ },
+ "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/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.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.8.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
+ "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "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/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/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/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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/std-env": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+ "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+ "dev": true,
+ "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/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.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "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/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tailwind-merge": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz",
+ "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/postcss-selector-parser": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
+ "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
+ "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"
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
+ "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
+ "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.11",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.11.tgz",
+ "integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==",
+ "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/tinybench": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz",
+ "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz",
+ "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.12.tgz",
+ "integrity": "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.12"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.12",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.12.tgz",
+ "integrity": "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ==",
+ "dev": true,
+ "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/tough-cookie": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
+ "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "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.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "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.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.60.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz",
+ "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.60.1",
+ "@typescript-eslint/parser": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1"
+ },
+ "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 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
+ "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
+ "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
+ "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.7",
+ "resolved": "https://registry.npmjs.org/urdf-loader/-/urdf-loader-0.12.7.tgz",
+ "integrity": "sha512-9RQDhWt4x9K6R3uHjv68NLwX+JDm3A2XjhksrMgq6xl9NJcwqV21+htHU0cbG619hnqH1d4mUMFuqHo22R8DMA==",
+ "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.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "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": "8.0.16",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
+ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.3",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz",
+ "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/mocker": "5.0.1",
+ "chai": "^6.2.2",
+ "es-module-lexer": "^2.3.2",
+ "expect-type": "^1.4.0",
+ "magic-string": "^1.2.3",
+ "obug": "^2.1.4",
+ "picomatch": "^4.0.7",
+ "std-env": "^4.2.0",
+ "tinybench": "6.1.4",
+ "tinyexec": "1.3.0",
+ "tinyglobby": "^0.2.17",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "5.0.1",
+ "@vitest/browser-preview": "5.0.1",
+ "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
+ "@vitest/coverage-istanbul": "5.0.1",
+ "@vitest/coverage-v8": "5.0.1",
+ "@vitest/ui": "5.0.1",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/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/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/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/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/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.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.14",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+ "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+ "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..29069686163dd036251d448745587f944b8c6a76
--- /dev/null
+++ b/package.json
@@ -0,0 +1,91 @@
+{
+ "name": "vite_react_shadcn_ts",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "test": "vitest run",
+ "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": "^7.18.3",
+ "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": "^4.3.1",
+ "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",
+ "jsdom": "^28.1.0",
+ "lovable-tagger": "^1.3.0",
+ "postcss": "^8.5.25",
+ "tailwindcss": "^3.4.11",
+ "typescript": "^5.5.3",
+ "typescript-eslint": "^8.0.1",
+ "vite": "^8.0.16",
+ "vitest": "^5.0.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..f53f6fbd886844ae2dbbd1906cadea82823e1adb
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,68 @@
+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 TeleopStopNotice from "@/components/TeleopStopNotice";
+import UpdateNotice from "@/components/UpdateNotice";
+import { TooltipProvider } from "@radix-ui/react-tooltip";
+import { ApiProvider } from "./contexts/ApiContext";
+import { HfAuthProvider } from "./contexts/HfAuthContext";
+import { OnboardingProvider } from "./contexts/OnboardingContext";
+
+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..02b7fb1595199194ed3296b96d291d47febde1b9
--- /dev/null
+++ b/src/components/SingleTabGuard.tsx
@@ -0,0 +1,141 @@
+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]);
+
+ // A non-primary tab renders only the blocker, not `children`. Anything the
+ // app mounts as a descendant (a Radix modal that sets `pointer-events: none`
+ // on `body`, the onboarding welcome dialog and `?` launcher) would otherwise
+ // sit behind this overlay and, in the modal case, kill the "Use this tab"
+ // button along with the rest of the page.
+ return (
+ <>
+ {isPrimary && 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/TeleopStopNotice.tsx b/src/components/TeleopStopNotice.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d8a12bd6c53f10d360f77c02d929c8625f32866e
--- /dev/null
+++ b/src/components/TeleopStopNotice.tsx
@@ -0,0 +1,36 @@
+import { useEffect } from "react";
+import { useToast } from "@/hooks/use-toast";
+
+const FLAG = "lelab:teleop-stopped";
+
+/**
+ * One-time confirmation that teleoperation was stopped during the previous
+ * page's unload (a browser navigation, reload, or tab close from the
+ * teleoperation page set a sessionStorage flag, since React cleanup can't run
+ * in those cases). On the next fresh load we surface a toast wherever the user
+ * landed, then clear the flag. In-app navigation away from teleop toasts
+ * directly and never sets the flag, so this never double-fires.
+ */
+const TeleopStopNotice = () => {
+ const { toast } = useToast();
+
+ useEffect(() => {
+ let stopped = false;
+ try {
+ stopped = sessionStorage.getItem(FLAG) === "1";
+ if (stopped) sessionStorage.removeItem(FLAG);
+ } catch {
+ /* sessionStorage unavailable — nothing to show */
+ }
+ if (stopped) {
+ toast({
+ title: "Teleoperation stopped",
+ description: "The arm was disconnected cleanly when you left the page.",
+ });
+ }
+ }, [toast]);
+
+ return null;
+};
+
+export default TeleopStopNotice;
diff --git a/src/components/UpdateNotice.tsx b/src/components/UpdateNotice.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a9c026f32b102fb3957d01a2d8696278dc79f136
--- /dev/null
+++ b/src/components/UpdateNotice.tsx
@@ -0,0 +1,194 @@
+import { useState } from "react";
+import { Loader2, Copy, Sparkles, ChevronRight } from "lucide-react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { useApi } from "@/contexts/ApiContext";
+import { useToast } from "@/hooks/use-toast";
+import { useUpdateCheck } from "@/hooks/useUpdateCheck";
+
+/**
+ * App-level popup that notifies the user when a newer LeLab is available on
+ * GitHub. Offers a copy-able upgrade command, a best-effort "Update now" button
+ * (runs the pip upgrade on the backend), and a "don't ask again" opt-out.
+ */
+const UpdateNotice = () => {
+ const { status, open, dismiss } = useUpdateCheck();
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const { toast } = useToast();
+ const [dontAsk, setDontAsk] = useState(false);
+ const [updating, setUpdating] = useState(false);
+ const [output, setOutput] = useState(null);
+
+ if (!status) return null;
+
+ const behind =
+ typeof status.commits_behind === "number" && status.commits_behind > 0
+ ? `${status.commits_behind} commit${status.commits_behind === 1 ? "" : "s"} behind`
+ : "A new version is available";
+
+ const copyCommand = async () => {
+ if (!status.update_command) return;
+ try {
+ await navigator.clipboard.writeText(status.update_command);
+ toast({
+ title: "Copied",
+ description: "Update command copied to clipboard.",
+ });
+ } catch {
+ toast({
+ title: "Copy failed",
+ description: "Select and copy the command manually.",
+ variant: "destructive",
+ });
+ }
+ };
+
+ const runUpdate = async () => {
+ setUpdating(true);
+ setOutput(null);
+ try {
+ const r = await fetchWithHeaders(`${baseUrl}/system/update`, {
+ method: "POST",
+ });
+ const body: { success: boolean; message: string; output: string } =
+ await r.json();
+ if (body.success) {
+ toast({ title: "Updated", description: body.message });
+ dismiss(false);
+ } else {
+ setOutput(body.output || body.message);
+ toast({
+ title: "Update failed",
+ description: body.message,
+ variant: "destructive",
+ });
+ }
+ } catch (e) {
+ toast({
+ title: "Update failed",
+ description: e instanceof Error ? e.message : String(e),
+ variant: "destructive",
+ });
+ } finally {
+ setUpdating(false);
+ }
+ };
+
+ return (
+ {
+ if (!o && !updating) dismiss(dontAsk);
+ }}
+ >
+ e.preventDefault()}
+ >
+
+
+
+ LeLab update available
+
+
+ You're {behind} 😱.
+
+ Update to get the latest fixes and features 🤗.
+ {status.compare_url && (
+ <>
+ {" "}
+
+ See what changed
+
+ .
+ >
+ )}
+
+
+
+
+
+
+
+ Or update manually
+
+
+
+
+ {status.update_command}
+
+
+
+
+
+
+
+
+ {output && (
+
+ {output}
+
+ )}
+
+
+
+ setDontAsk(v === true)}
+ className="border-slate-300 data-[state=checked]:bg-slate-300 data-[state=checked]:text-slate-900"
+ />
+ Don't ask me again
+
+
+ dismiss(dontAsk)}
+ disabled={updating}
+ className="text-slate-300 hover:bg-slate-700 hover:text-white"
+ >
+ Later
+
+ {status.can_auto_update && (
+
+ {updating ? (
+ <>
+
+ Updating…
+ >
+ ) : (
+ "Update now"
+ )}
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default UpdateNotice;
diff --git a/src/components/UrdfViewer.tsx b/src/components/UrdfViewer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..30e2b5cf6080c400b6821eeea1e9884db8bcf304
--- /dev/null
+++ b/src/components/UrdfViewer.tsx
@@ -0,0 +1,310 @@
+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,
+ fitGridToRobot,
+ 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);
+
+ // Rescale the ground grid to this robot's actual unit scale — it
+ // starts as a 1x1 placeholder since it's created before any model
+ // is loaded (see createUrdfViewer).
+ fitGridToRobot(viewer, center, boundingBox.min.y, maxDim);
+
+ // 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/BrowserCameraPreview.test.tsx b/src/components/control/BrowserCameraPreview.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c6bca6f70948d48ad4759e1eb4bad48682a3df24
--- /dev/null
+++ b/src/components/control/BrowserCameraPreview.test.tsx
@@ -0,0 +1,51 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot, Root } from "react-dom/client";
+import { afterEach, beforeEach, expect, test, vi } from "vitest";
+import BrowserCameraPreview from "./BrowserCameraPreview";
+import CameraRotationSelect from "./CameraRotationSelect";
+vi.mock("@/hooks/useCameraStream", () => ({ useCameraStream: () => ({ videoRef: React.createRef(), hasError: false }) }));
+let root: Root;
+let container: HTMLDivElement;
+beforeEach(() => { Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); container = document.createElement("div"); root = createRoot(container); });
+afterEach(() => act(() => root.unmount()));
+test("the Rotation control exposes four clockwise values and emits a number", () => {
+ const change = vi.fn();
+ act(() => root.render( ));
+ const select = container.querySelector("select")!;
+ expect(container.querySelector("label")!.htmlFor).toBe(select.id);
+ expect(select.value).toBe("180");
+ expect(Array.from(select.options).map((option) => option.text)).toEqual(["0° clockwise", "90° clockwise", "180° clockwise", "270° clockwise"]);
+ act(() => { select.value = "90"; select.dispatchEvent(new Event("change", { bubbles: true })); });
+ expect(change).toHaveBeenCalledWith(90);
+});
+test("a quarter turn fills the rotated recorded frame", () => {
+ act(() => root.render( ));
+ const video = container.querySelector("video")!, wrapper = video.parentElement!;
+ expect(wrapper.style.aspectRatio).toBe("480 / 640");
+ expect(video.style.objectFit).toBe("cover");
+ expect(video.style.maxWidth).toBe("none");
+ expect(video.style.transform).toBe("translate(-50%, -50%) rotate(90deg)");
+ expect(parseFloat(video.style.width)).toBeCloseTo(640 / 480 * 100);
+ expect(parseFloat(video.style.height)).toBeCloseTo(480 / 640 * 100);
+});
+test("a stream at another size keeps the recorded aspect ratio", () => {
+ act(() => root.render( ));
+ const video = container.querySelector("video")!;
+ Object.defineProperty(video, "videoWidth", { value: 1280 });
+ Object.defineProperty(video, "videoHeight", { value: 720 });
+ act(() => video.dispatchEvent(new Event("loadedmetadata")));
+ expect(video.parentElement!.style.aspectRatio).toBe("640 / 480");
+ expect(video.style.width).toBe("100%");
+ expect(video.style.objectFit).toBe("cover");
+});
+test("the reference ghost sits over the video with its rotation and opacity", () => {
+ act(() => root.render( ));
+ const video = container.querySelector("video")!, ghost = container.querySelector("img")!;
+ expect(ghost.getAttribute("src")).toBe("blob:ref");
+ expect(ghost.style.opacity).toBe("0.3");
+ expect(ghost.style.transform).toBe(video.style.transform);
+ expect(ghost.style.width).toBe(video.style.width);
+ act(() => root.render( ));
+ expect(container.querySelector("img")).toBeNull();
+});
diff --git a/src/components/control/BrowserCameraPreview.tsx b/src/components/control/BrowserCameraPreview.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b2b693345fe31f0076ae739d9fc30cc06be24544
--- /dev/null
+++ b/src/components/control/BrowserCameraPreview.tsx
@@ -0,0 +1,75 @@
+import React from "react";
+import { VideoOff } from "lucide-react";
+import { useCameraStream } from "@/hooks/useCameraStream";
+import { CameraRotation, cameraPreviewGeometry, cameraRotationError } from "@/lib/cameraConfig";
+
+interface Props {
+ deviceId: string;
+ paused?: boolean;
+ rotation?: CameraRotation;
+ width?: number;
+ height?: number;
+ className?: string;
+ fallback?: string;
+ /** Saved reference frame, drawn over the live video as a translucent ghost. */
+ ghostSrc?: string;
+ ghostOpacity?: number;
+}
+
+/**
+ * Browser streams contain raw frames, so only browser previews use CSS rotation.
+ * The box keeps the recorded capture size's shape even when the browser stream
+ * has another size, so the preview frames the scene the way recording will.
+ */
+export default function BrowserCameraPreview({
+ deviceId,
+ paused = false,
+ rotation = 0,
+ width = 640,
+ height = 480,
+ className = "",
+ fallback,
+ ghostSrc,
+ ghostOpacity = 0.5,
+}: Props) {
+ const rotationError = cameraRotationError({ rotation });
+ const { videoRef, hasError } = useCameraStream(deviceId, paused || !!rotationError, { width, height });
+ const geometry = rotationError
+ ? { aspectRatio: `${width} / ${height}`, videoStyle: {} }
+ : cameraPreviewGeometry(width, height, rotation);
+ const showVideo = !rotationError && !paused && deviceId && !hasError;
+ return (
+
+ {showVideo ? (
+ <>
+
+ {ghostSrc && (
+
+ )}
+ >
+ ) : (
+
+
+
+ {rotationError ?? fallback ?? (paused ? "Preview paused" : deviceId ? "Preview failed" : "No camera selected")}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/control/CameraFeed.tsx b/src/components/control/CameraFeed.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ccf3a333a4335ad5317e76cef07f443a9e8ccecc
--- /dev/null
+++ b/src/components/control/CameraFeed.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+import BrowserCameraPreview from "./BrowserCameraPreview";
+import { CameraRotation } from "@/lib/cameraConfig";
+
+interface CameraFeedProps {
+ deviceId: string;
+ label?: string;
+ rotation?: CameraRotation;
+ width?: number;
+ height?: number;
+}
+
+const CameraFeed: React.FC = ({ deviceId, label, rotation, width, height }) => (
+
+
+ {label && (
+
{label}
+ )}
+
+);
+
+export default CameraFeed;
diff --git a/src/components/control/CameraRotationSelect.tsx b/src/components/control/CameraRotationSelect.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a664eb8623f5d4f8aec6cca6633aac0dc9e36eec
--- /dev/null
+++ b/src/components/control/CameraRotationSelect.tsx
@@ -0,0 +1,30 @@
+import React, { useId } from "react";
+import { Label } from "@/components/ui/label";
+import { CAMERA_ROTATIONS, CameraRotation, cameraRotation } from "@/lib/cameraConfig";
+
+interface Props {
+ rotation: CameraRotation | null;
+ onChange: (rotation: CameraRotation) => void;
+ disabled?: boolean;
+}
+
+export default function CameraRotationSelect({ rotation, onChange, disabled }: Props) {
+ const id = useId();
+ return (
+
+ Rotation
+ onChange(cameraRotation(Number(event.target.value)))}
+ className="bg-gray-800 border border-gray-700 rounded text-white text-xs h-7 px-2"
+ >
+ {rotation === null && Choose rotation }
+ {CAMERA_ROTATIONS.map((angle) => (
+ {angle}° clockwise
+ ))}
+
+
+ );
+}
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/RecordingCameraFeed.test.tsx b/src/components/control/RecordingCameraFeed.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a000493341cd931a2c32f02361a31aabccb45b20
--- /dev/null
+++ b/src/components/control/RecordingCameraFeed.test.tsx
@@ -0,0 +1,27 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { expect, test } from "vitest";
+import RecordingCameraFeed from "./RecordingCameraFeed";
+import { recordingCameraWindows } from "@/lib/cameraConfig";
+
+test("recording feeds use each camera's output ratio without rotating corrected backend frames", () => {
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
+ const windows = recordingCameraWindows(["front", "wrist"], {
+ front: { type: "opencv", width: 640, height: 480, rotation: 0 },
+ wrist: { type: "opencv", width: 640, height: 480, rotation: 90 },
+ }, { w: 1000, h: 600 });
+ const container = document.createElement("div"), root = createRoot(container);
+ act(() => root.render(<>{Object.entries(windows).map(([name, dimensions]) => (
+
+ ))}>));
+ const images = Array.from(container.querySelectorAll("img"));
+ expect(images).toHaveLength(2);
+ expect(parseFloat(images[0].parentElement!.style.width) / parseFloat(images[0].parentElement!.style.height)).toBeCloseTo(4 / 3, 2);
+ expect(parseFloat(images[1].parentElement!.style.width) / parseFloat(images[1].parentElement!.style.height)).toBeCloseTo(3 / 4, 2);
+ for (const image of images) {
+ expect(image.className).toContain("object-contain");
+ expect(image.style.transform).toBe("");
+ }
+ act(() => root.unmount());
+});
diff --git a/src/components/control/RecordingCameraFeed.tsx b/src/components/control/RecordingCameraFeed.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ec6b3bf8db7b70a98fd317f2ea7e9b11744f4ae2
--- /dev/null
+++ b/src/components/control/RecordingCameraFeed.tsx
@@ -0,0 +1,75 @@
+import React, { useState, useEffect, useRef, useCallback } from "react";
+
+interface CameraFeedProps {
+ baseUrl: string;
+ name: string;
+ // False during the preparing phase: show an empty slot until the camera is
+ // connected and streaming. True once recording/resetting, when frames flow.
+ live: boolean;
+ // Pixel size computed by the parent to fit the available space. The box is
+ // sized to the camera's aspect ratio, so the video fills it without letterbox.
+ width: number;
+ height: number;
+}
+
+// Renders one recording camera's window at an explicit size. During preparing it
+// shows an empty placeholder; once live it plays the backend MJPEG stream. The
+// browser renders a `multipart/x-mixed-replace` response natively in an ,
+// so we just point it at /camera-feed/{name}. If the stream errors before frames
+// flow (camera still warming up), retry with a cache-busting key after a delay.
+const RecordingCameraFeed: React.FC = ({
+ baseUrl,
+ name,
+ live,
+ width,
+ height,
+}) => {
+ const [reloadKey, setReloadKey] = useState(0);
+ const [hasError, setHasError] = useState(false);
+ const retryRef = useRef(null);
+
+ const src = `${baseUrl}/camera-feed/${encodeURIComponent(name)}?k=${reloadKey}`;
+
+ useEffect(() => {
+ return () => {
+ if (retryRef.current) window.clearTimeout(retryRef.current);
+ };
+ }, []);
+
+ const handleError = useCallback(() => {
+ setHasError(true);
+ if (retryRef.current) window.clearTimeout(retryRef.current);
+ retryRef.current = window.setTimeout(() => {
+ setHasError(false);
+ setReloadKey((k) => k + 1);
+ }, 1500);
+ }, []);
+
+ // 0 before the first measurement; skip rendering a zero-size box.
+ if (width <= 0 || height <= 0) return null;
+
+ return (
+
+ {!live ? (
+
Getting ready…
+ ) : hasError ? (
+
Connecting feed…
+ ) : (
+
+ )}
+
+ {name}
+
+
+ );
+};
+
+export default RecordingCameraFeed;
diff --git a/src/components/control/TeleopCameraPanel.tsx b/src/components/control/TeleopCameraPanel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4e61377dcd1c23d95fd70b852f85ab93030acbfa
--- /dev/null
+++ b/src/components/control/TeleopCameraPanel.tsx
@@ -0,0 +1,104 @@
+import React, { useState } from "react";
+import { RefreshCw } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { useRobots } from "@/hooks/useRobots";
+import { useAvailableCameras } from "@/hooks/useAvailableCameras";
+import { previewDeviceId } from "@/lib/cameraConfig";
+import CameraFeed from "./CameraFeed";
+
+/**
+ * Optional live camera panel for the teleoperation page. Off by default so we
+ * never call getUserMedia just by landing on the page (same consent pattern as
+ * the calibration camera toggle). Teleoperation opens no cv2 cameras, so the
+ * browser can stream them directly while the arm runs.
+ *
+ * A strict mirror of the selected robot's configured cameras: one live feed per
+ * camera on the robot record (e.g. "wrist_cam", "webcam"), stacked vertically.
+ * If the robot has none configured it shows nothing — teleop never surfaces a
+ * device that wasn't deliberately added to the robot.
+ */
+const TeleopCameraPanel: React.FC = () => {
+ const [enabled, setEnabled] = useState(false);
+ // Bumped by the retry button to remount the feeds (a fresh getUserMedia
+ // attempt) — useful if a camera was unplugged and reconnected.
+ const [reloadKey, setReloadKey] = useState(0);
+ const { selectedRecord, isLoading: robotsLoading } = useRobots();
+ const { cameras: availableCameras } = useAvailableCameras({ enabled });
+
+ // Feeds come solely from the robot's configured cameras, streamed by their
+ // stored browser device_id or, in a browser that doesn't know that ID, by
+ // their cv2 index. A configured camera whose device is currently absent still
+ // shows (name + failed-preview placeholder), so the user can tell it's
+ // expected but not detected.
+ const configured = selectedRecord?.cameras ?? [];
+ const feeds = configured.map((c) => ({
+ key: c.id,
+ name: c.name,
+ deviceId: previewDeviceId(c, availableCameras),
+ rotation: c.rotation,
+ width: c.width,
+ height: c.height,
+ }));
+
+ return (
+
+
+
Cameras
+
+ {enabled && feeds.length > 0 && (
+ setReloadKey((k) => k + 1)}
+ className="h-9 w-9 text-gray-400 hover:text-white flex-shrink-0"
+ title="Retry camera feeds (e.g. after reconnecting a camera)"
+ aria-label="Retry camera feeds"
+ >
+
+
+ )}
+
+ {enabled ? "On" : "Off"}
+
+
+
+
+
+ {enabled ? (
+ feeds.length > 0 ? (
+
+ {feeds.map((feed) => (
+
+ ))}
+
+ ) : (
+
+ {robotsLoading
+ ? "Loading robot..."
+ : "No cameras configured for this robot. Add them during calibration to see live feeds here."}
+
+ )
+ ) : (
+
+ Turn on to watch your cameras while you teleoperate.
+
+ )}
+
+ );
+};
+
+export default TeleopCameraPanel;
diff --git a/src/components/control/VisualizerPanel.tsx b/src/components/control/VisualizerPanel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9dca7861db3cc1e0cf8def14d85bdcc0083859d1
--- /dev/null
+++ b/src/components/control/VisualizerPanel.tsx
@@ -0,0 +1,52 @@
+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;
+ /** Optional content rendered as a column beside the 3D viewer (e.g. a camera panel). */
+ rightSlot?: React.ReactNode;
+}
+
+const VisualizerPanel: React.FC = ({
+ onGoBack,
+ className,
+ rightSlot,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
Teleoperation
+
+
+
+
+
+ {rightSlot && (
+
{rightSlot}
+ )}
+
+ );
+};
+
+export default VisualizerPanel;
diff --git a/src/components/dataset/DeleteDatasetDialog.test.tsx b/src/components/dataset/DeleteDatasetDialog.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fe87aca510f9e6be6444cabf78547e37dd6a03b0
--- /dev/null
+++ b/src/components/dataset/DeleteDatasetDialog.test.tsx
@@ -0,0 +1,66 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { expect, test, vi } from "vitest";
+
+import DeleteDatasetDialog from "./DeleteDatasetDialog";
+
+const fetchWithHeaders = vi.fn(async (_url: string, _init: { body: string }) => ({ ok: true, json: async () => ({ success: true }) }));
+
+vi.mock("@/contexts/ApiContext", () => ({
+ useApi: () => ({ baseUrl: "http://api", fetchWithHeaders }),
+}));
+vi.mock("@/hooks/use-toast", () => ({ useToast: () => ({ toast: () => {} }) }));
+
+const render = async (onHub: boolean, onDeleted = () => {}) => {
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ await act(async () => {
+ root.render(
+ {}}
+ onDeleted={onDeleted}
+ />,
+ );
+ });
+ return root;
+};
+
+const deleteButton = () =>
+ [...document.querySelectorAll("button")].find((b) => b.textContent === "Delete")!;
+
+test("a local-only dataset offers no Hub checkbox and deletes locally", async () => {
+ fetchWithHeaders.mockClear();
+ const onDeleted = vi.fn();
+ const root = await render(false, onDeleted);
+ expect(document.body.textContent).not.toContain("Also delete this dataset from the HuggingFace Hub");
+
+ await act(async () => deleteButton().click());
+
+ expect(JSON.parse(fetchWithHeaders.mock.calls[0][1].body)).toEqual({
+ dataset_repo_id: "me/demo",
+ delete_from_hub: false,
+ });
+ expect(onDeleted).toHaveBeenCalled();
+ act(() => root.unmount());
+});
+
+test("deleting from the Hub stays disabled until the repo id is typed", async () => {
+ const root = await render(true);
+ const checkbox = document.querySelector("#delete-from-hub")!;
+ await act(async () => checkbox.click());
+ expect(deleteButton().disabled).toBe(true);
+
+ const input = document.querySelector("input")!;
+ await act(async () => {
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!.call(input, "me/demo");
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ });
+ expect(deleteButton().disabled).toBe(false);
+ act(() => root.unmount());
+});
diff --git a/src/components/dataset/DeleteDatasetDialog.tsx b/src/components/dataset/DeleteDatasetDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bd77f6b0945342da04de4bf492db72738507be12
--- /dev/null
+++ b/src/components/dataset/DeleteDatasetDialog.tsx
@@ -0,0 +1,139 @@
+import React, { useState } from "react";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { useApi } from "@/contexts/ApiContext";
+import { useToast } from "@/hooks/use-toast";
+
+interface DeleteDatasetDialogProps {
+ repoId: string;
+ onHub: boolean;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onDeleted: () => void;
+}
+
+/** Confirms and performs the removal of a local dataset, optionally from the Hub too. */
+const DeleteDatasetDialog: React.FC = ({
+ repoId,
+ onHub,
+ open,
+ onOpenChange,
+ onDeleted,
+}) => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const { toast } = useToast();
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [deleteFromHub, setDeleteFromHub] = useState(false);
+ const [confirmText, setConfirmText] = useState("");
+
+ const reset = () => {
+ setDeleteFromHub(false);
+ setConfirmText("");
+ };
+
+ const handleDelete = async () => {
+ setIsDeleting(true);
+ try {
+ const response = await fetchWithHeaders(`${baseUrl}/delete-dataset`, {
+ method: "POST",
+ body: JSON.stringify({ dataset_repo_id: repoId, delete_from_hub: deleteFromHub }),
+ });
+ const data = await response.json();
+ if (response.ok && data.success) {
+ toast({
+ title: "Dataset Deleted",
+ description: deleteFromHub
+ ? `${repoId} has been removed from disk and the Hub.`
+ : `${repoId} has been removed from disk.`,
+ });
+ onDeleted();
+ } else {
+ toast({
+ title: "Delete Failed",
+ description: data.message || "Could not delete the dataset.",
+ variant: "destructive",
+ });
+ }
+ } catch {
+ toast({
+ title: "Connection Error",
+ description: "Could not connect to the backend server.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsDeleting(false);
+ onOpenChange(false);
+ reset();
+ }
+ };
+
+ return (
+ {
+ onOpenChange(next);
+ if (!next) reset();
+ }}
+ >
+
+
+ Delete dataset from disk?
+
+ This permanently removes {repoId} from your local cache. This action cannot be undone.
+
+
+ {onHub && (
+
+
+ setDeleteFromHub(checked === true)}
+ />
+
+ Also delete this dataset from the HuggingFace Hub
+
+
+ {deleteFromHub && (
+
+
+ Type {repoId} to confirm
+
+ setConfirmText(e.target.value)}
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+ )}
+
+ )}
+
+
+ Keep dataset
+
+
+ {isDeleting ? "Deleting…" : "Delete"}
+
+
+
+
+ );
+};
+
+export default DeleteDatasetDialog;
diff --git a/src/components/dataset/EpisodeList.tsx b/src/components/dataset/EpisodeList.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..36186b60acfbb144d7997c77334c65b7654cdfb1
--- /dev/null
+++ b/src/components/dataset/EpisodeList.tsx
@@ -0,0 +1,89 @@
+import React, { useEffect, useRef } from "react";
+import { Pencil } from "lucide-react";
+import { EpisodeSummary, formatDuration } from "@/lib/datasetApi";
+
+interface EpisodeListProps {
+ episodes: EpisodeSummary[];
+ selected: number | null;
+ onSelect: (episodeIndex: number) => void;
+ /** Shows an edit button on the selected episode's task when set. */
+ onEditTask?: (episodeIndex: number) => void;
+}
+
+const EpisodeList: React.FC = ({ episodes, selected, onSelect, onEditTask }) => {
+ const activeRef = useRef(null);
+
+ // Follow the selection. Paging with Previous/Next, or opening a link straight
+ // to episode 40, would otherwise leave the highlighted row scrolled out of
+ // sight — the list would look like it never moved.
+ useEffect(() => {
+ activeRef.current?.scrollIntoView({ block: "nearest" });
+ }, [selected]);
+
+ if (episodes.length === 0) {
+ return (
+
+ No episodes in this dataset.
+
+ );
+ }
+
+ return (
+
+
+
+ Episodes
+
+ {episodes.length}
+
+ {/* Tailwind arbitrary values take underscores where the CSS needs spaces:
+ `calc(100vh-280px)` is not valid CSS and the rule is dropped. */}
+
+ {episodes.map((ep) => {
+ const active = ep.episode_index === selected;
+ return (
+
+
onSelect(ep.episode_index)}
+ className="flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-left focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-orange-500"
+ >
+
+ {ep.episode_index}
+
+
+ {ep.tasks.length > 0 ? ep.tasks.join(", ") : "—"}
+
+
+ {formatDuration(ep.duration_s)}
+
+
+ {active && onEditTask && (
+
onEditTask(ep.episode_index)}
+ aria-label={`Edit the task of episode ${ep.episode_index}`}
+ className="mr-2 rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-gray-200 focus:outline-none focus-visible:ring-1 focus-visible:ring-orange-500"
+ >
+
+
+ )}
+
+ );
+ })}
+
+
+ );
+};
+
+export default EpisodeList;
diff --git a/src/components/dataset/EpisodeTaskDialog.test.tsx b/src/components/dataset/EpisodeTaskDialog.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9b1e34f47fc869e354a750f6bfc882791ae02218
--- /dev/null
+++ b/src/components/dataset/EpisodeTaskDialog.test.tsx
@@ -0,0 +1,61 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { expect, test } from "vitest";
+
+import EpisodeTaskDialog from "./EpisodeTaskDialog";
+
+test("saves the task for the browsed episode or for every episode", async () => {
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ const saved: [string, number | null][] = [];
+
+ try {
+ await act(async () => {
+ root.render(
+ {}}
+ onSave={(task, episodeIndex) => {
+ saved.push([task, episodeIndex]);
+ }}
+ />,
+ );
+ });
+
+ const input = document.getElementById("episode-task") as HTMLInputElement;
+ const saveButton = () =>
+ [...document.body.querySelectorAll("button")].find(
+ (b) => b.textContent === "Save task",
+ ) as HTMLButtonElement;
+ const type = async (value: string) => {
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
+ await act(async () => {
+ setter.call(input, value);
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ });
+ };
+
+ expect(input.value).toBe("pick the cube");
+
+ await type(" ");
+ expect(saveButton().disabled).toBe(true);
+
+ await type(" pick the bowl ");
+ await act(async () => saveButton().click());
+ expect(saved).toEqual([["pick the bowl", 3]]);
+
+ await act(async () => (document.getElementById("episode-task-all") as HTMLButtonElement).click());
+ await act(async () => saveButton().click());
+ expect(saved.at(-1)).toEqual(["pick the bowl", null]);
+ } finally {
+ await act(async () => root.unmount());
+ host.remove();
+ }
+});
diff --git a/src/components/dataset/EpisodeTaskDialog.tsx b/src/components/dataset/EpisodeTaskDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8ca08ecb6154f686896c9d173fc5b1ee9a72a4aa
--- /dev/null
+++ b/src/components/dataset/EpisodeTaskDialog.tsx
@@ -0,0 +1,106 @@
+import { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+interface EpisodeTaskDialogProps {
+ repoId: string;
+ episodeIndex: number;
+ currentTask: string;
+ totalEpisodes: number;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ /** `episodeIndex` is null when the task applies to every episode. */
+ onSave: (task: string, episodeIndex: number | null) => void;
+}
+
+export default function EpisodeTaskDialog({
+ repoId,
+ episodeIndex,
+ currentTask,
+ totalEpisodes,
+ open,
+ onOpenChange,
+ onSave,
+}: EpisodeTaskDialogProps) {
+ const [task, setTask] = useState("");
+ const [allEpisodes, setAllEpisodes] = useState(false);
+
+ useEffect(() => {
+ if (open) {
+ setTask(currentTask);
+ setAllEpisodes(false);
+ }
+ }, [open, currentTask]);
+
+ const canSave = task.trim() !== "";
+
+ const handleSave = () => {
+ if (!canSave) return;
+ onSave(task.trim(), allEpisodes ? null : episodeIndex);
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+ Edit task
+
+ The task is the instruction a policy is trained on, saved with each episode of{" "}
+ {repoId}.
+
+
+
+
+
+
+ Task
+
+ setTask(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === "Enter") handleSave();
+ }}
+ className="border-gray-700 bg-gray-800 text-white"
+ />
+
+ {totalEpisodes > 1 && (
+
+ setAllEpisodes(checked === true)}
+ className="border-gray-500"
+ />
+
+ Apply to all {totalEpisodes} episodes, not only episode {episodeIndex}
+
+
+ )}
+
+
+
+ onOpenChange(false)}>
+ Cancel
+
+
+ Save task
+
+
+
+
+ );
+}
diff --git a/src/components/dataset/EpisodeViewer.test.tsx b/src/components/dataset/EpisodeViewer.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c8b3f8576e76da29af3994c124b35fbc88f30170
--- /dev/null
+++ b/src/components/dataset/EpisodeViewer.test.tsx
@@ -0,0 +1,52 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { expect, test, vi } from "vitest";
+import EpisodeViewer from "./EpisodeViewer";
+import { EpisodeDetail } from "@/lib/datasetApi";
+
+vi.mock("@/contexts/ApiContext", () => ({
+ useApi: () => ({ baseUrl: "http://localhost:8000" }),
+}));
+vi.mock("./FilmStrip", () => ({ default: () => null }));
+vi.mock("./MotionTrace", () => ({ default: () => null }));
+vi.mock("./ReplayButton", () => ({ default: () => null }));
+
+test.each([0, 4])("playback loops at file end with an episode offset of %s seconds", async (offset) => {
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
+ const host = document.createElement("div");
+ const root = createRoot(host);
+ const detail: EpisodeDetail = {
+ success: true, repo_id: "owner/demo", episode_index: 1,
+ length: 10, duration_s: 1, tasks: [], fps: 10, robot_type: "test",
+ cameras: [{ name: "top", chunk: 0, file_index: 0,
+ from_timestamp: offset, to_timestamp: offset + 1 }],
+ prev_episode: 0, next_episode: null, total_episodes: 2,
+ };
+ act(() => root.render( {}} />));
+ const video = host.querySelector("video")!;
+ let paused = false;
+ let ended = false;
+ Object.defineProperties(video, {
+ paused: { get: () => paused },
+ ended: { get: () => ended },
+ });
+ video.play = vi.fn(async () => { paused = false; });
+ try {
+ act(() => video.dispatchEvent(new Event("play")));
+ // At natural EOF the browser is already paused when timeupdate runs,
+ // then delivers pause and ended. Seeking alone does not resume playback.
+ paused = true;
+ ended = true;
+ video.currentTime = offset + 1;
+ await act(async () => {
+ for (const type of ["timeupdate", "pause", "ended"]) {
+ video.dispatchEvent(new Event(type));
+ }
+ });
+ expect(video.currentTime).toBe(offset);
+ expect(paused).toBe(false);
+ } finally {
+ act(() => root.unmount());
+ }
+});
diff --git a/src/components/dataset/EpisodeViewer.tsx b/src/components/dataset/EpisodeViewer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..655f3fc5dfe7190a818ace8e356d0e0f92c43c70
--- /dev/null
+++ b/src/components/dataset/EpisodeViewer.tsx
@@ -0,0 +1,435 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import {
+ ChevronLeft,
+ ChevronRight,
+ Grid2X2,
+ Loader2,
+ Maximize2,
+ Pause,
+ Play,
+ SkipBack,
+ SkipForward,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useApi } from "@/contexts/ApiContext";
+import { EpisodeDetail, frameUrl, videoUrl } from "@/lib/datasetApi";
+import FilmStrip from "./FilmStrip";
+import MotionTrace from "./MotionTrace";
+import ReplayButton from "./ReplayButton";
+
+const RATES = [0.5, 1, 2] as const;
+type Rate = (typeof RATES)[number];
+
+/**
+ * Secondaries are nudged only when they drift past this. Chasing every
+ * timeupdate would thrash the decoder for sub-frame corrections nobody sees.
+ */
+const SYNC_DEADBAND_S = 0.2;
+
+interface EpisodeViewerProps {
+ repoId: string;
+ detail: EpisodeDetail;
+ onNavigate: (episodeIndex: number) => void;
+}
+
+const EpisodeViewer: React.FC = ({ repoId, detail, onNavigate }) => {
+ const { baseUrl } = useApi();
+ const cams = detail.cameras;
+
+ const [activeCam, setActiveCam] = useState(cams[0]?.name ?? "");
+ const [showAll, setShowAll] = useState(false);
+ const [playing, setPlaying] = useState(false);
+ const [rate, setRate] = useState(1);
+ const [currentFrame, setCurrentFrame] = useState(0);
+
+ const driverRef = useRef(null);
+ const secondaryRefs = useRef>({});
+
+ // Episode changed (or its cameras did) → fall back to a camera that exists.
+ useEffect(() => {
+ if (!cams.some((c) => c.name === activeCam)) setActiveCam(cams[0]?.name ?? "");
+ }, [cams, activeCam]);
+
+ const activeInfo = useMemo(
+ () => cams.find((c) => c.name === activeCam) ?? cams[0] ?? null,
+ [cams, activeCam],
+ );
+
+ const fps = detail.fps ?? 20;
+ // The mp4 holds several episodes back to back; this one occupies
+ // [fromTs, toTs). Everything below is expressed against that window.
+ const fromTs = activeInfo?.from_timestamp ?? 0;
+ const toTs = activeInfo?.to_timestamp ?? fromTs + (detail.duration_s ?? 0);
+
+ const seekToFrame = useCallback(
+ (frameIndex: number) => {
+ const v = driverRef.current;
+ if (!v) return;
+ const clamped = Math.max(0, Math.min(frameIndex, Math.max(detail.length - 1, 0)));
+ // Seek to the frame's CENTER, not its boundary. A boundary seek sits
+ // epsilon away from `clamped / fps`, so the timeupdate recompute
+ // (floor((t - fromTs) * fps)) can floor to the previous frame: stepping
+ // right went +1 then snapped back, stepping left jumped 2. Mid-frame,
+ // floor(clamped + 0.5) is immune to float error in either direction.
+ v.currentTime = fromTs + (clamped + 0.5) / fps;
+ setCurrentFrame(clamped);
+ },
+ [fromTs, fps, detail.length],
+ );
+
+ const handleLoadedMetadata = useCallback(() => {
+ const v = driverRef.current;
+ if (!v) return;
+ v.currentTime = fromTs;
+ // A fresh element resets playbackRate to 1, which used to leave the pills
+ // reading "2x" while playback ran at 1x.
+ v.playbackRate = rate;
+ setCurrentFrame(0);
+ }, [fromTs, rate]);
+
+ // Move to the new episode's window when the selection changes.
+ //
+ // The element is keyed on the *file*, so paging between episodes that share
+ // an mp4 doesn't remount it and `loadedMetadata` won't fire again — this is
+ // what actually puts the player on the new episode. Keying on the episode
+ // instead would rebuild the player every time, which silently drops playback
+ // (a new element is always paused) and leaves a frozen frame behind.
+ useEffect(() => {
+ const v = driverRef.current;
+ if (!v) return;
+ const wasPlaying = !v.paused;
+ const goToEpisode = () => {
+ v.currentTime = fromTs;
+ v.playbackRate = rate;
+ setCurrentFrame(0);
+ // Keep playing across the switch rather than stranding the user on a
+ // still frame with the button claiming it's playing.
+ if (wasPlaying || playing) void v.play().catch(() => setPlaying(false));
+ };
+ if (v.readyState >= 1) {
+ goToEpisode();
+ } else {
+ v.addEventListener("loadedmetadata", goToEpisode, { once: true });
+ return () => v.removeEventListener("loadedmetadata", goToEpisode);
+ }
+ // `playing` is deliberately not a dep: this runs on episode/camera change,
+ // not every play/pause.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [detail.episode_index, activeCam, fromTs]);
+
+ const handleTimeUpdate = useCallback(() => {
+ const v = driverRef.current;
+ if (!v) return;
+ // Loop inside this episode's window rather than running on into the next
+ // episode's footage, which shares the file.
+ // At file end, let onEnded rewind and resume after the browser pauses.
+ if (v.ended) return;
+ if (v.currentTime >= toTs) {
+ v.currentTime = fromTs;
+ setCurrentFrame(0);
+ return;
+ }
+ setCurrentFrame(Math.max(0, Math.floor((v.currentTime - fromTs) * fps)));
+
+ if (!showAll) return;
+ // Each camera sits at its own offset inside its own file, so a secondary is
+ // synced by mapping through *its* from_timestamp — not by copying
+ // currentTime, which would land it in a neighbouring episode.
+ for (const c of cams) {
+ if (c.name === activeCam) continue;
+ const el = secondaryRefs.current[c.name];
+ if (!el) continue;
+ const target = (c.from_timestamp ?? 0) + (v.currentTime - fromTs);
+ if (Math.abs(el.currentTime - target) > SYNC_DEADBAND_S) el.currentTime = target;
+ el.playbackRate = v.playbackRate;
+ if (playing && el.paused) void el.play().catch(() => undefined);
+ if (!playing && !el.paused) el.pause();
+ }
+ }, [toTs, fromTs, fps, showAll, cams, activeCam, playing]);
+
+ const togglePlay = useCallback(() => {
+ const v = driverRef.current;
+ if (!v) return;
+ if (v.paused) {
+ void v.play().then(
+ () => setPlaying(true),
+ () => setPlaying(false),
+ );
+ } else {
+ v.pause();
+ setPlaying(false);
+ }
+ }, []);
+
+ // The arm replays at the recording's speed; run the footage alongside it.
+ const playFromStart = useCallback(() => {
+ const v = driverRef.current;
+ if (!v) return;
+ setRate(1);
+ seekToFrame(0);
+ void v.play().then(
+ () => setPlaying(true),
+ () => setPlaying(false),
+ );
+ }, [seekToFrame]);
+
+ const pauseFootage = useCallback(() => {
+ driverRef.current?.pause();
+ setPlaying(false);
+ }, []);
+
+ // Keep the element's rate in step with the pills across re-renders and
+ // camera swaps.
+ useEffect(() => {
+ if (driverRef.current) driverRef.current.playbackRate = rate;
+ }, [rate, activeCam]);
+
+ // Space toggles playback, except while typing in a field.
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ const el = e.target as HTMLElement | null;
+ if (el && ["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName)) return;
+ if (e.code === "Space") {
+ e.preventDefault();
+ togglePlay();
+ } else if (e.code === "ArrowLeft") {
+ seekToFrame(currentFrame - 1);
+ } else if (e.code === "ArrowRight") {
+ seekToFrame(currentFrame + 1);
+ }
+ };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [togglePlay, seekToFrame, currentFrame]);
+
+ if (!activeInfo) {
+ return (
+
+ This episode has no camera videos on disk.
+
+ );
+ }
+
+ const secondaries = cams.filter((c) => c.name !== activeCam);
+
+ return (
+
+ {/* Camera + view controls */}
+
+
+ {cams.map((c) => (
+ setActiveCam(c.name)}
+ className={`rounded px-3 py-1 text-xs font-medium transition ${
+ c.name === activeCam
+ ? "bg-gray-800 text-white"
+ : "text-gray-400 hover:text-gray-200"
+ }`}
+ >
+ {c.name}
+
+ ))}
+
+
+ {cams.length > 1 && (
+
setShowAll((s) => !s)}
+ className={`h-7 gap-1.5 border-gray-800 bg-gray-950 text-xs hover:bg-gray-900 ${
+ showAll ? "text-orange-400" : "text-gray-400"
+ }`}
+ >
+
+ All cameras
+
+ )}
+
+
+
+
+ {RATES.map((r) => (
+ setRate(r)}
+ className={`rounded px-2.5 py-1 text-xs font-medium tabular-nums transition ${
+ r === rate ? "bg-gray-800 text-white" : "text-gray-400 hover:text-gray-200"
+ }`}
+ >
+ {r}x
+
+ ))}
+
+
+
+
+ {/* Viewport */}
+
1
+ ? "grid grid-cols-1 gap-2 sm:grid-cols-2"
+ : "grid grid-cols-1"
+ }
+ >
+
+ 1 ? "max-h-[22vh]" : "max-h-[42vh]"
+ }`}
+ preload="auto"
+ playsInline
+ muted
+ onLoadedMetadata={handleLoadedMetadata}
+ onTimeUpdate={handleTimeUpdate}
+ onEnded={(e) => {
+ const v = e.currentTarget;
+ v.currentTime = fromTs;
+ setCurrentFrame(0);
+ void v.play().catch(() => setPlaying(false));
+ }}
+ onPlay={() => setPlaying(true)}
+ onPause={() => setPlaying(false)}
+ />
+
+ {activeCam}
+
+
+
+ {showAll &&
+ secondaries.map((c) => (
+
+ {
+ secondaryRefs.current[c.name] = el;
+ }}
+ src={videoUrl(baseUrl, repoId, c.name, c.chunk, c.file_index)}
+ poster={frameUrl(baseUrl, repoId, detail.episode_index, c.name, 0, 640)}
+ className="max-h-[22vh] w-auto max-w-full bg-black object-contain"
+ preload="auto"
+ playsInline
+ muted
+ onLoadedMetadata={(e) => {
+ e.currentTarget.currentTime = c.from_timestamp ?? 0;
+ }}
+ />
+
+ {c.name}
+
+
+ ))}
+
+
+ {/* Transport */}
+
+
+ {playing ? : }
+
+
seekToFrame(currentFrame - 1)}
+ className="h-8 w-8 border-gray-800 bg-gray-950 p-0 text-gray-300 hover:bg-gray-900"
+ title="Previous frame (←)"
+ >
+
+
+
seekToFrame(currentFrame + 1)}
+ className="h-8 w-8 border-gray-800 bg-gray-950 p-0 text-gray-300 hover:bg-gray-900"
+ title="Next frame (→)"
+ >
+
+
+
+
seekToFrame(Number(e.target.value))}
+ className="mx-2 h-1 flex-1 cursor-pointer appearance-none rounded bg-gray-800 accent-orange-500"
+ aria-label="Scrub episode"
+ />
+
+
+ {currentFrame} / {Math.max(detail.length - 1, 0)}
+
+
+
+
+
+
+
+ {/* Episode paging */}
+
+ detail.prev_episode !== null && onNavigate(detail.prev_episode)}
+ className="h-7 gap-1 border-gray-800 bg-gray-950 text-xs text-gray-300 hover:bg-gray-900 disabled:opacity-40"
+ >
+
+ Previous
+
+
+ Episode {detail.episode_index} of {detail.total_episodes}
+
+ detail.next_episode !== null && onNavigate(detail.next_episode)}
+ className="h-7 gap-1 border-gray-800 bg-gray-950 text-xs text-gray-300 hover:bg-gray-900 disabled:opacity-40"
+ >
+ Next
+
+
+
+
+ );
+};
+
+export default EpisodeViewer;
diff --git a/src/components/dataset/FilmStrip.tsx b/src/components/dataset/FilmStrip.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9475aa24eb3196b9a74627fd95117c9fdb25e956
--- /dev/null
+++ b/src/components/dataset/FilmStrip.tsx
@@ -0,0 +1,105 @@
+import React, { useEffect, useState } from "react";
+import { Loader2 } from "lucide-react";
+import { useApi } from "@/contexts/ApiContext";
+import { Thumbnail, getThumbnails } from "@/lib/datasetApi";
+
+interface FilmStripProps {
+ repoId: string;
+ episodeIndex: number;
+ camera: string;
+ /** Episode-relative frame currently on screen; the nearest thumb highlights. */
+ currentFrame: number;
+ onSeekFrame: (frameIndex: number) => void;
+ count?: number;
+}
+
+/**
+ * Thumbnails across the episode. One request for the whole strip — the backend
+ * decodes them in a single pass over the mp4 rather than reopening it per frame.
+ */
+const FilmStrip: React.FC = ({
+ repoId,
+ episodeIndex,
+ camera,
+ currentFrame,
+ onSeekFrame,
+ count = 12,
+}) => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const [thumbs, setThumbs] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setLoading(true);
+ getThumbnails(baseUrl, fetchWithHeaders, repoId, episodeIndex, camera, count, controller.signal)
+ .then((r) => {
+ if (controller.signal.aborted) return;
+ setThumbs(r.success ? r.thumbnails : []);
+ })
+ .catch(() => {
+ if (!controller.signal.aborted) setThumbs([]);
+ })
+ .finally(() => {
+ if (!controller.signal.aborted) setLoading(false);
+ });
+ return () => controller.abort();
+ }, [baseUrl, fetchWithHeaders, repoId, episodeIndex, camera, count]);
+
+ // Highlight the thumb nearest the playhead rather than the one before it, so
+ // the marker tracks what's actually on screen.
+ const activeIdx = thumbs.reduce(
+ (best, t, i) =>
+ Math.abs(t.frame_index - currentFrame) <
+ Math.abs((thumbs[best]?.frame_index ?? Infinity) - currentFrame)
+ ? i
+ : best,
+ 0,
+ );
+
+ if (loading && thumbs.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ if (thumbs.length === 0) {
+ return (
+
+ No thumbnails
+
+ );
+ }
+
+ return (
+
+ {thumbs.map((t, i) => (
+
onSeekFrame(t.frame_index)}
+ title={`Frame ${t.frame_index}`}
+ className={`group relative flex-none overflow-hidden rounded transition ${
+ i === activeIdx
+ ? "ring-2 ring-orange-500"
+ : "ring-1 ring-gray-800 hover:ring-gray-600"
+ } focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-400`}
+ >
+
+
+ {t.frame_index}
+
+
+ ))}
+
+ );
+};
+
+export default FilmStrip;
diff --git a/src/components/dataset/MergeDatasetsDialog.test.tsx b/src/components/dataset/MergeDatasetsDialog.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a94ffb30f195657440958f82d39c0cbb4a4e520b
--- /dev/null
+++ b/src/components/dataset/MergeDatasetsDialog.test.tsx
@@ -0,0 +1,60 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { expect, test } from "vitest";
+
+import MergeDatasetsDialog from "./MergeDatasetsDialog";
+
+test("opens with the browsed local dataset retained as the first merge source", async () => {
+ Object.assign(globalThis, {
+ IS_REACT_ACT_ENVIRONMENT: true,
+ ResizeObserver: class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ },
+ });
+ Element.prototype.scrollIntoView = () => {};
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+
+ try {
+ await act(async () => {
+ root.render(
+ {}}
+ onMerge={async () => {}}
+ />,
+ );
+ });
+
+ expect(document.body.textContent).toContain("local/browsed is included");
+ expect(document.body.textContent).not.toContain("local/additional");
+ expect(document.body.textContent).toContain("Select at least one additional dataset.");
+ const mergeButton = [...document.body.querySelectorAll("button")].find((button) =>
+ button.textContent?.includes("Merge datasets"),
+ ) as HTMLButtonElement;
+ expect(mergeButton.disabled).toBe(true);
+
+ const addButton = [...document.body.querySelectorAll("button")].find((button) =>
+ button.textContent?.includes("Add dataset"),
+ ) as HTMLButtonElement;
+ await act(async () => {
+ addButton.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
+ addButton.click();
+ });
+ expect(document.body.querySelector("[cmdk-input]")).not.toBeNull();
+ expect(document.body.textContent).toContain("local/additional");
+ expect(document.body.textContent).not.toContain("Create new dataset");
+ } finally {
+ await act(async () => root.unmount());
+ host.remove();
+ }
+});
diff --git a/src/components/dataset/MergeDatasetsDialog.tsx b/src/components/dataset/MergeDatasetsDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e1b2e6240f8e19d1a85e113840cb79bc492052dd
--- /dev/null
+++ b/src/components/dataset/MergeDatasetsDialog.tsx
@@ -0,0 +1,169 @@
+import { useEffect, useMemo, useState } from "react";
+import { Loader2, Plus, X } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import DatasetPicker from "@/components/landing/DatasetPicker";
+import { DatasetItem } from "@/lib/replayApi";
+
+interface MergeDatasetsDialogProps {
+ datasets: DatasetItem[];
+ selectedRepoId: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onMerge: (sourceRepoIds: string[], outputName: string) => Promise;
+}
+
+export default function MergeDatasetsDialog({
+ datasets,
+ selectedRepoId,
+ open,
+ onOpenChange,
+ onMerge,
+}: MergeDatasetsDialogProps) {
+ const [selected, setSelected] = useState([selectedRepoId]);
+ const [outputName, setOutputName] = useState("");
+ const [merging, setMerging] = useState(false);
+ const [error, setError] = useState(null);
+
+ const candidates = useMemo(
+ () =>
+ datasets.filter(
+ (dataset) =>
+ (dataset.source === "local" || dataset.source === "both") && !selected.includes(dataset.repo_id),
+ ),
+ [datasets, selected],
+ );
+ const canMerge = selected.length >= 2 && /^[A-Za-z0-9._-]+$/.test(outputName) && !merging;
+
+ const addDataset = (repoId: string) => {
+ setError(null);
+ setSelected((current) => [...current, repoId]);
+ };
+
+ const removeDataset = (repoId: string) => {
+ setError(null);
+ setSelected((current) => current.filter((id) => id !== repoId));
+ };
+
+ const reset = () => {
+ setSelected([selectedRepoId]);
+ setOutputName("");
+ setMerging(false);
+ setError(null);
+ };
+
+ useEffect(() => {
+ if (open) {
+ setSelected([selectedRepoId]);
+ setOutputName("");
+ setMerging(false);
+ setError(null);
+ }
+ }, [open, selectedRepoId]);
+
+ const handleOpenChange = (nextOpen: boolean) => {
+ if (!merging && !nextOpen) reset();
+ onOpenChange(nextOpen);
+ };
+
+ const handleMerge = async () => {
+ if (!canMerge) return;
+ setMerging(true);
+ try {
+ await onMerge(selected, outputName);
+ reset();
+ onOpenChange(false);
+ } catch (caughtError) {
+ setError(caughtError instanceof Error ? caughtError.message : "Could not merge the selected datasets.");
+ } finally {
+ setMerging(false);
+ }
+ };
+
+ return (
+
+
+
+ Merge local datasets
+
+ {selectedRepoId} is included. Select at least one more local dataset. Their source files stay
+ unchanged; the merged dataset is saved locally.
+
+
+
+
+
Source datasets
+
+
{selectedRepoId}
+ {selected.slice(1).map((repoId) => (
+
+ {repoId}
+ removeDataset(repoId)}
+ disabled={merging}
+ className="text-gray-400 hover:text-white"
+ >
+
+
+
+ ))}
+
+
addDataset(item.repo_id)}
+ >
+
+
+ Add dataset
+
+
+
Select at least one additional dataset.
+
+
+
+ Merged dataset name
+
+
{
+ setError(null);
+ setOutputName(event.target.value.replace(/[^A-Za-z0-9._-]/g, "_"));
+ }}
+ placeholder="combined_pick_place"
+ disabled={merging}
+ className="border-gray-700 bg-gray-800 text-white"
+ />
+
+ Saved locally as local/{outputName || "name"}.
+
+
+ {error &&
{error}
}
+
+
+
+ handleOpenChange(false)} disabled={merging}>
+ Cancel
+
+
+ {merging && }
+ {merging ? "Merging…" : "Merge datasets"}
+
+
+
+
+ );
+}
diff --git a/src/components/dataset/MotionTrace.tsx b/src/components/dataset/MotionTrace.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..de3f50456c8699238112b731cb0678d0adbfe41e
--- /dev/null
+++ b/src/components/dataset/MotionTrace.tsx
@@ -0,0 +1,134 @@
+import React, { useEffect, useMemo, useState } from "react";
+import {
+ Area,
+ AreaChart,
+ ReferenceLine,
+ ResponsiveContainer,
+ Tooltip,
+ YAxis,
+} from "recharts";
+import { useApi } from "@/contexts/ApiContext";
+import { getMotion } from "@/lib/datasetApi";
+
+interface MotionTraceProps {
+ repoId: string;
+ episodeIndex: number;
+ currentFrame: number;
+ onSeekFrame: (frameIndex: number) => void;
+}
+
+/**
+ * Aggregate joint motion across the episode — one value per frame, summing the
+ * absolute change of every joint in the commanded action vector.
+ *
+ * Collapsing all joints into a single trace is the point: it makes the shape of
+ * a demonstration legible at a glance (idle head, the busy middle, idle tail)
+ * without asking anyone to read six overlapping lines.
+ */
+const MotionTrace: React.FC = ({
+ repoId,
+ episodeIndex,
+ currentFrame,
+ onSeekFrame,
+}) => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const [motion, setMotion] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ setError(null);
+ getMotion(baseUrl, fetchWithHeaders, repoId, episodeIndex, controller.signal)
+ .then((r) => {
+ if (controller.signal.aborted) return;
+ if (!r.success) {
+ setMotion(null);
+ setError(r.message ?? "No action stream");
+ return;
+ }
+ setMotion(r.motion);
+ })
+ .catch((e) => {
+ if (controller.signal.aborted) return;
+ setMotion(null);
+ setError(e instanceof Error ? e.message : "No action stream");
+ });
+ return () => controller.abort();
+ }, [baseUrl, fetchWithHeaders, repoId, episodeIndex]);
+
+ const data = useMemo(
+ () => (motion ?? []).map((v, i) => ({ frame: i, motion: v })),
+ [motion],
+ );
+
+ if (error) {
+ return (
+
+ Joint motion unavailable — {error}
+
+ );
+ }
+
+ if (!motion) {
+ return
;
+ }
+
+ return (
+
+
+
+ Joint motion
+
+
+ frame {currentFrame}
+
+
+
+
+ {
+ // activeLabel is the `frame` value under the cursor.
+ const f = e?.activeLabel;
+ if (f !== undefined && f !== null) onSeekFrame(Number(f));
+ }}
+ >
+
+
+
+
+
+
+
+ [v.toFixed(2), "motion"]}
+ labelFormatter={(l) => `Frame ${l}`}
+ />
+
+
+
+
+
+
+ );
+};
+
+export default MotionTrace;
diff --git a/src/components/dataset/MoveEpisodesDialog.test.tsx b/src/components/dataset/MoveEpisodesDialog.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..cbe095dec968b83800c93d61d8182bbe7c17a1d1
--- /dev/null
+++ b/src/components/dataset/MoveEpisodesDialog.test.tsx
@@ -0,0 +1,118 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { expect, test } from "vitest";
+
+import type { DatasetItem, MoveDestination } from "@/lib/replayApi";
+
+import { parseEpisodeList } from "@/lib/datasetApi";
+
+import MoveEpisodesDialog from "./MoveEpisodesDialog";
+
+test("parses episode lists and ranges", () => {
+ expect(parseEpisodeList("3, 1, 5-7, 3", 10)).toEqual([1, 3, 5, 6, 7]);
+ expect(parseEpisodeList(" 2 - 4 ,", 10)).toEqual([2, 3, 4]);
+ expect(parseEpisodeList("", 10)).toBeNull();
+ expect(parseEpisodeList("10", 10)).toBeNull();
+ expect(parseEpisodeList("4-2", 10)).toBeNull();
+ expect(parseEpisodeList("1, x", 10)).toBeNull();
+});
+
+const DATASETS: DatasetItem[] = [
+ { repo_id: "local/demo", source: "local", last_modified: "", private: false },
+ { repo_id: "local/target", source: "local", last_modified: "", private: false },
+];
+
+test("opens on the browsed episode and refuses to move every episode", async () => {
+ Object.assign(globalThis, {
+ IS_REACT_ACT_ENVIRONMENT: true,
+ ResizeObserver: class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ },
+ });
+ Element.prototype.scrollIntoView = () => {};
+ const host = document.createElement("div");
+ document.body.append(host);
+ const root = createRoot(host);
+ const moved: [number[], MoveDestination][] = [];
+
+ try {
+ await act(async () => {
+ root.render(
+ {}}
+ onMove={async (indices, destination) => {
+ moved.push([indices, destination]);
+ }}
+ />,
+ );
+ });
+
+ const episodes = document.getElementById("move-episodes") as HTMLInputElement;
+ const moveButton = () =>
+ [...document.body.querySelectorAll("button")].find((b) =>
+ b.textContent?.startsWith("Move "),
+ ) as HTMLButtonElement;
+ const type = async (input: HTMLInputElement, value: string) => {
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
+ await act(async () => {
+ setter.call(input, value);
+ input.dispatchEvent(new Event("input", { bubbles: true }));
+ });
+ };
+ const openPicker = async () => {
+ const trigger = [...document.body.querySelectorAll("button")].find((b) =>
+ b.textContent?.includes("Select or create a dataset") || b.textContent?.startsWith("local/"),
+ ) as HTMLButtonElement;
+ await act(async () => {
+ trigger.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
+ trigger.click();
+ });
+ };
+
+ expect(episodes.value).toBe("1");
+ expect(moveButton().disabled).toBe(true);
+
+ // The source never shows up as a destination.
+ await openPicker();
+ const items = [...document.body.querySelectorAll("[cmdk-item]")].map((i) => i.textContent);
+ expect(items).toEqual(["local/target"]);
+
+ // Create a new dataset by typing a name in the shared picker.
+ const search = document.body.querySelector("[cmdk-input]") as HTMLInputElement;
+ await type(search, "rejected takes");
+ await act(async () => {
+ search.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
+ });
+ expect(document.body.textContent).toContain("Saved locally as local/rejected_takes");
+ expect(moveButton().disabled).toBe(false);
+
+ await type(episodes, "0-1");
+ expect(document.body.textContent).toContain("At least one episode must stay in the dataset.");
+ expect(moveButton().disabled).toBe(true);
+
+ await type(episodes, "1");
+ await act(async () => moveButton().click());
+ expect(moved).toEqual([[[1], { newName: "rejected_takes" }]]);
+
+ // Pick an existing dataset instead.
+ await openPicker();
+ const target = [...document.body.querySelectorAll("[cmdk-item]")].find(
+ (i) => i.textContent === "local/target",
+ ) as HTMLElement;
+ await act(async () => target.click());
+ expect(document.body.textContent).toContain("Merged into local/target");
+ await act(async () => moveButton().click());
+ expect(moved[1]).toEqual([[1], { existingRepoId: "local/target" }]);
+ } finally {
+ await act(async () => root.unmount());
+ host.remove();
+ }
+});
diff --git a/src/components/dataset/MoveEpisodesDialog.tsx b/src/components/dataset/MoveEpisodesDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eaf27386118fa5c61265f7dc115997ebf496ce13
--- /dev/null
+++ b/src/components/dataset/MoveEpisodesDialog.tsx
@@ -0,0 +1,172 @@
+import { useEffect, useMemo, useState } from "react";
+import { ChevronsUpDown, Loader2 } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import DatasetPicker from "@/components/landing/DatasetPicker";
+import { parseEpisodeList } from "@/lib/datasetApi";
+import { DatasetItem, MoveDestination } from "@/lib/replayApi";
+
+interface MoveEpisodesDialogProps {
+ datasets: DatasetItem[];
+ repoId: string;
+ totalEpisodes: number;
+ currentEpisode: number | null;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onMove: (episodeIndices: number[], destination: MoveDestination) => Promise;
+}
+
+export default function MoveEpisodesDialog({
+ datasets,
+ repoId,
+ totalEpisodes,
+ currentEpisode,
+ open,
+ onOpenChange,
+ onMove,
+}: MoveEpisodesDialogProps) {
+ const [episodesText, setEpisodesText] = useState("");
+ const [destination, setDestination] = useState(null);
+ const [moving, setMoving] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (open) {
+ setEpisodesText(currentEpisode === null ? "" : String(currentEpisode));
+ setDestination(null);
+ setMoving(false);
+ setError(null);
+ }
+ }, [open, currentEpisode]);
+
+ const candidates = useMemo(() => datasets.filter((d) => d.repo_id !== repoId), [datasets, repoId]);
+ const episodes = parseEpisodeList(episodesText, totalEpisodes);
+ const movesEverything = episodes !== null && episodes.length >= totalEpisodes;
+ const canMove = episodes !== null && !movesEverything && destination !== null && !moving;
+ const pickDestination = (next: MoveDestination) => {
+ setError(null);
+ setDestination(next);
+ };
+
+ const handleOpenChange = (nextOpen: boolean) => {
+ if (moving) return;
+ onOpenChange(nextOpen);
+ };
+
+ const handleMove = async () => {
+ if (!canMove || episodes === null || destination === null) return;
+ setMoving(true);
+ try {
+ await onMove(episodes, destination);
+ onOpenChange(false);
+ } catch (caughtError) {
+ setError(caughtError instanceof Error ? caughtError.message : "Could not move the episodes.");
+ } finally {
+ setMoving(false);
+ }
+ };
+
+ return (
+
+
+
+ Move episodes to another dataset
+
+ The episodes leave {repoId} and go to a new or an existing local dataset.
+ The episodes left in {repoId} are renumbered.
+
+
+
+
+
+
+ Episodes
+
+
{
+ setError(null);
+ setEpisodesText(event.target.value);
+ }}
+ placeholder="3, 7, 10-12"
+ disabled={moving}
+ className="border-gray-700 bg-gray-800 text-white"
+ />
+ {episodesText.trim() && episodes === null && (
+
+ Use episode numbers from 0 to {totalEpisodes - 1}, e.g. 3, 7, 10-12.
+
+ )}
+ {movesEverything && (
+
At least one episode must stay in the dataset.
+ )}
+
+
+
+
Destination
+
pickDestination({ existingRepoId: item.repo_id })}
+ onCreateNew={(name) => pickDestination({ newName: name })}
+ >
+
+
+ {destination === null
+ ? "Select or create a dataset…"
+ : "newName" in destination
+ ? `local/${destination.newName}`
+ : destination.existingRepoId}
+
+
+
+
+ {destination !== null && (
+
+ {"newName" in destination ? (
+ <>
+ Saved locally as local/{destination.newName}.
+ >
+ ) : (
+ <>
+ Merged into {destination.existingRepoId} after its own episodes. It must
+ have the same fps, features and robot type.
+ >
+ )}
+
+ )}
+
+ {error &&
{error}
}
+
+
+
+ handleOpenChange(false)} disabled={moving}>
+ Cancel
+
+
+ {moving && }
+ {moving
+ ? "Moving…"
+ : `Move ${episodes?.length ?? 0} episode${episodes?.length === 1 ? "" : "s"}`}
+
+
+
+
+ );
+}
diff --git a/src/components/dataset/ReplayButton.test.tsx b/src/components/dataset/ReplayButton.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f03104ae2d21d9455f03aa5c1a86357ae51c82cb
--- /dev/null
+++ b/src/components/dataset/ReplayButton.test.tsx
@@ -0,0 +1,148 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot, Root } from "react-dom/client";
+import { afterEach, beforeEach, expect, test, vi } from "vitest";
+import ReplayButton from "./ReplayButton";
+import { RobotRecord } from "@/hooks/useRobots";
+
+const backend = vi.hoisted(() => ({
+ active: false,
+ startOk: true,
+ statusFails: false,
+ requests: [] as { path: string; body?: unknown }[],
+}));
+const robot = vi.hoisted(() => ({ current: null as RobotRecord | null }));
+const toast = vi.hoisted(() => vi.fn());
+const api = vi.hoisted(() => ({ baseUrl: "http://localhost:8000", fetchWithHeaders: () => {} }));
+
+vi.mock("@/contexts/ApiContext", () => ({ useApi: () => api }));
+vi.mock("@/hooks/useRobots", () => ({ useRobots: () => ({ selectedRecord: robot.current }) }));
+vi.mock("@/hooks/use-toast", () => ({ useToast: () => ({ toast }) }));
+vi.mock("@/lib/apiClient", () => ({
+ apiRequest: async (_baseUrl: string, _fetcher: unknown, path: string, opts?: { body?: unknown }) => {
+ backend.requests.push({ path, body: opts?.body });
+ if (path === "/replay-status") {
+ if (backend.statusFails) throw new Error("network");
+ return { replay_active: backend.active, error: null };
+ }
+ if (path === "/start-replay") {
+ backend.active = backend.startOk;
+ return { success: backend.startOk, message: "A replay is already running" };
+ }
+ return { success: true, message: "Replay stopped" };
+ },
+}));
+
+const calibrated: RobotRecord = {
+ name: "arm",
+ leader_port: "/dev/leader",
+ follower_port: "/dev/follower",
+ leader_config: "leader.json",
+ follower_config: "follower.json",
+ cameras: [],
+ is_clean: true,
+};
+
+let host: HTMLDivElement;
+let root: Root;
+const onStart = vi.fn();
+const onEnd = vi.fn();
+
+const button = (label: string) =>
+ [...host.querySelectorAll("button")].find((b) => b.textContent?.includes(label));
+
+async function mount() {
+ await act(async () => {
+ root.render( );
+ });
+}
+
+async function poll() {
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(500);
+ });
+}
+
+beforeEach(() => {
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
+ vi.useFakeTimers();
+ Object.assign(backend, { active: false, startOk: true, statusFails: false, requests: [] });
+ robot.current = calibrated;
+ toast.mockClear();
+ onStart.mockClear();
+ onEnd.mockClear();
+ host = document.createElement("div");
+ root = createRoot(host);
+});
+
+afterEach(() => {
+ act(() => root.unmount());
+ vi.useRealTimers();
+});
+
+test("hidden until a calibrated robot is selected", async () => {
+ robot.current = null;
+ await mount();
+ expect(host.querySelector("button")).toBeNull();
+});
+
+test("a replay already running stays stoppable without a selected robot", async () => {
+ robot.current = null;
+ backend.active = true;
+ await mount();
+ expect(button("Stop robot")).toBeDefined();
+});
+
+test("starting replays this episode and runs the footage alongside", async () => {
+ await mount();
+ await act(async () => button("Play on robot")!.click());
+ expect(backend.requests.at(-1)).toMatchObject({
+ path: "/start-replay",
+ body: { dataset_repo_id: "local/ds", episode_index: 2, follower_port: "/dev/follower" },
+ });
+ expect(onStart).toHaveBeenCalledOnce();
+ expect(button("Stop robot")).toBeDefined();
+});
+
+test("Stop stays until the status shows the arm released", async () => {
+ backend.active = true;
+ await mount();
+ await act(async () => button("Stop robot")!.click());
+ await poll();
+ expect(button("Stop robot")).toBeDefined();
+
+ backend.active = false;
+ await poll();
+ expect(button("Play on robot")).toBeDefined();
+});
+
+test("a failed status poll keeps the Stop button", async () => {
+ backend.active = true;
+ await mount();
+ backend.statusFails = true;
+ await poll();
+ expect(button("Stop robot")).toBeDefined();
+});
+
+test("a refused start reports it and offers Play again", async () => {
+ backend.startOk = false;
+ await mount();
+ await act(async () => button("Play on robot")!.click());
+ expect(onStart).not.toHaveBeenCalled();
+ expect(toast).toHaveBeenCalledWith(
+ expect.objectContaining({ description: "A replay is already running" }),
+ );
+ expect(button("Play on robot")).toBeDefined();
+});
+
+test("the footage is told to stop once the replay ends or is stopped", async () => {
+ await mount();
+ await act(async () => button("Play on robot")!.click());
+ await poll();
+ expect(onEnd).not.toHaveBeenCalled();
+
+ await act(async () => button("Stop robot")!.click());
+ backend.active = false;
+ await poll();
+ expect(onEnd).toHaveBeenCalledOnce();
+});
diff --git a/src/components/dataset/ReplayButton.tsx b/src/components/dataset/ReplayButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..86fffbbb7177598b50eedc804a833eb245e65b7a
--- /dev/null
+++ b/src/components/dataset/ReplayButton.tsx
@@ -0,0 +1,150 @@
+import React, { useEffect, useRef, useState } from "react";
+import { Bot, Loader2, Square } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useApi } from "@/contexts/ApiContext";
+import { useToast } from "@/hooks/use-toast";
+import { useRobots } from "@/hooks/useRobots";
+import { apiRequest } from "@/lib/apiClient";
+
+interface ReplayResponse {
+ success: boolean;
+ message: string;
+}
+
+interface ReplayStatus {
+ replay_active: boolean;
+ error: string | null;
+}
+
+interface ReplayButtonProps {
+ repoId: string;
+ episodeIndex: number;
+ /** Called once the arm starts moving, so the footage can run alongside it. */
+ onStart: () => void;
+ /** Called once the arm is released, whether the replay ended, failed or was stopped. */
+ onEnd: () => void;
+}
+
+/**
+ * Plays the episode's recorded actions on the selected follower arm. Only
+ * shown when that arm is ready to move.
+ */
+const ReplayButton: React.FC = ({ repoId, episodeIndex, onStart, onEnd }) => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const { selectedRecord: robot } = useRobots();
+ const { toast } = useToast();
+ const [phase, setPhase] = useState<"idle" | "starting" | "playing">("idle");
+ // Paging to another episode while the arm connects must not drag the
+ // footage back to the one being replayed.
+ const episodeRef = useRef(episodeIndex);
+ episodeRef.current = episodeIndex;
+
+ // A replay keeps running when this button unmounts (another dataset, another
+ // page), so pick it back up to keep it stoppable.
+ useEffect(() => {
+ apiRequest(baseUrl, fetchWithHeaders, "/replay-status")
+ .then((status) => {
+ if (status.replay_active) setPhase("playing");
+ })
+ .catch(() => undefined);
+ }, [baseUrl, fetchWithHeaders]);
+
+ // Poll only while the arm moves, to notice when the episode ends or fails.
+ useEffect(() => {
+ if (phase !== "playing") return;
+ const timer = setInterval(async () => {
+ try {
+ const status = await apiRequest(baseUrl, fetchWithHeaders, "/replay-status");
+ if (status.replay_active) return;
+ setPhase("idle");
+ onEnd();
+ if (status.error) {
+ toast({ title: "Replay failed", description: status.error, variant: "destructive" });
+ }
+ } catch {
+ // Keep the Stop button: the next poll retries.
+ }
+ }, 500);
+ return () => clearInterval(timer);
+ }, [phase, baseUrl, fetchWithHeaders, toast, onEnd]);
+
+ // Back to Play only once the status poll sees the arm released.
+ const stop = async () => {
+ try {
+ const res = await apiRequest(baseUrl, fetchWithHeaders, "/stop-replay", {
+ method: "POST",
+ action: "Stop",
+ });
+ if (!res.success) throw new Error(res.message);
+ } catch (e) {
+ toast({
+ title: "Could not stop the robot",
+ description: e instanceof Error ? e.message : String(e),
+ variant: "destructive",
+ });
+ }
+ };
+
+ // A running replay stays stoppable even once the robot is no longer selected.
+ if (phase === "playing") {
+ return (
+
+
+ Stop robot
+
+ );
+ }
+
+ if (!robot?.is_clean) return null;
+
+ const start = async () => {
+ setPhase("starting");
+ try {
+ const res = await apiRequest(baseUrl, fetchWithHeaders, "/start-replay", {
+ method: "POST",
+ body: {
+ follower_port: robot.follower_port,
+ follower_config: robot.follower_config,
+ dataset_repo_id: repoId,
+ episode_index: episodeIndex,
+ },
+ action: "Replay",
+ });
+ if (!res.success) throw new Error(res.message);
+ setPhase("playing");
+ if (episodeRef.current === episodeIndex) onStart();
+ } catch (e) {
+ setPhase("idle");
+ toast({
+ title: "Could not replay this episode",
+ description: e instanceof Error ? e.message : String(e),
+ variant: "destructive",
+ });
+ }
+ };
+
+ return (
+
+ {phase === "starting" ? (
+
+ ) : (
+
+ )}
+ Play on robot
+
+ );
+};
+
+export default ReplayButton;
diff --git a/src/components/jobs/CheckpointDropdown.tsx b/src/components/jobs/CheckpointDropdown.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..36597a7802c353a2292ce9b983036a62f881050b
--- /dev/null
+++ b/src/components/jobs/CheckpointDropdown.tsx
@@ -0,0 +1,58 @@
+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",
+}) => {
+ // step 0 is the sentinel for an imported single-model checkpoint (lerobot
+ // never saves at step 0), so it has no meaningful step number — show
+ // "latest" instead. Real training checkpoints keep their step label.
+ const labelFor = (step: number) => (step === 0 ? "latest" : `step ${step}`);
+ const value = selectedStep != null ? String(selectedStep) : undefined;
+ return (
+ onChange(Number(v))}
+ disabled={disabled || checkpoints.length === 0}
+ >
+ e.stopPropagation()}
+ >
+
+
+
+ {checkpoints.map((c) => (
+ e.stopPropagation()}
+ >
+ {labelFor(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/ImportModelModal.tsx b/src/components/jobs/ImportModelModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b8b820d0e3f67c6f64bd3c31b4d5d2a7e3507cf2
--- /dev/null
+++ b/src/components/jobs/ImportModelModal.tsx
@@ -0,0 +1,121 @@
+import React, { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/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 { AlertTriangle, Download, Loader2 } from "lucide-react";
+import { useApi } from "@/contexts/ApiContext";
+import { importModel } from "@/lib/jobsApi";
+
+interface Props {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onImported: () => void;
+}
+
+const ImportModelModal: React.FC = ({ open, onOpenChange, onImported }) => {
+ const { baseUrl, fetchWithHeaders } = useApi();
+ const [source, setSource] = useState("");
+ const [name, setName] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSubmit = async () => {
+ const src = source.trim();
+ if (!src) return;
+ setSubmitting(true);
+ setError(null);
+ try {
+ await importModel(baseUrl, fetchWithHeaders, src, name.trim() || undefined);
+ setSource("");
+ setName("");
+ onOpenChange(false);
+ onImported();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : String(e));
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+ Import a model
+
+
+ Point at a local directory or a Hugging Face repo. It appears as a
+ job you can run inference on.
+
+
+
+
+
+
+ Local path or Hugging Face repo id
+
+ setSource(e.target.value)}
+ placeholder="/path/to/pretrained_model or user/my-policy"
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+
+
+ Display name (optional)
+
+ setName(e.target.value)}
+ placeholder="My imported policy"
+ className="bg-gray-800 border-gray-700 text-white"
+ />
+
+
+ {error ? (
+
+
+ {error}
+
+ ) : null}
+
+
+
+ {submitting ? (
+
+ ) : (
+
+ )}
+ {submitting ? "Importing…" : "Import"}
+
+ onOpenChange(false)}
+ variant="outline"
+ className="border-gray-500 px-8 text-zinc-400 bg-zinc-900 hover:bg-zinc-800"
+ >
+ Cancel
+
+
+
+
+
+ );
+};
+
+export default ImportModelModal;
diff --git a/src/components/jobs/JobCard.tsx b/src/components/jobs/JobCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2a0af722a9c3a554560bc386565e1ea314a36afc
--- /dev/null
+++ b/src/components/jobs/JobCard.tsx
@@ -0,0 +1,221 @@
+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 isImported = job.runner === "imported";
+ const importedSource = job.hf_repo_id || job.output_dir;
+ const stateLabel = isImported ? "Imported" : present.label;
+ 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 = isImported
+ ? importedSource
+ : 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 (isImported) {
+ if (window.confirm("Remove this imported model? The source files are left untouched."))
+ onDelete(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 (
+ {
+ if (!isImported) navigate(`/training/${job.id}`);
+ }}
+ className={`bg-slate-800/50 border-slate-700 rounded-xl transition-colors ${
+ isImported ? "" : "cursor-pointer hover:border-slate-500"
+ }`}
+ >
+
+
+
+
+ {stateLabel}
+
+ {job.runner === "hf_cloud" && job.hf_job_url ? (
+
+ e.stopPropagation()}
+ >
+
+
+
+ ) : (
+
+ {isRunning ? : }
+
+ )}
+
+
+
+ {job.name}
+
+ {/* Imported subtitles are file paths — truncate the *start* (rtl
+ flips the ellipsis to the left) so the more useful tail stays
+ visible. The leading LRM keeps the path's first "/" from being
+ bidi-reordered to the wrong end. */}
+
+ {isImported ? "\u200e" + subtitle : 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..0424464758747f54dc516b9f51d209a70f4ba0ce
--- /dev/null
+++ b/src/components/jobs/JobsSection.tsx
@@ -0,0 +1,435 @@
+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 ImportModelModal from "./ImportModelModal";
+import { useRobots } from "@/hooks/useRobots";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { ChevronRight, Download, 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 [importModalOpen, setImportModalOpen] = 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],
+ );
+ const importedJobs = useMemo(
+ () => filteredJobs.filter((j) => j.runner === "imported"),
+ [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"
+ />
+
+
setImportModalOpen(true)}
+ className="h-8 border-slate-700 bg-slate-800/50 text-slate-200 hover:text-white"
+ >
+
+ Import model
+
+
+
+
+
+
+
+ {error ? Couldn't load jobs: {error}
: null}
+
+
+
+
+ Local jobs ({localActive.length})
+
+
+ {localActive.length === 0 ? (
+
+ {query
+ ? "No local jobs match your search."
+ : "No active local jobs. Start one from the Training page."}
+
+ ) : (
+
+ {localActive.map((job) => (
+
+ ))}
+
+ )}
+
+
+
+ {importedJobs.length > 0 ? (
+ <>
+
+
+
+
+ Imported models ({importedJobs.length})
+
+
+
+ {importedJobs.map((job) => (
+
+ ))}
+
+
+
+ >
+ ) : null}
+
+
+
+
+
+
+ Online jobs (
+ {trackedCloudActive.length +
+ untrackedHubActive.length +
+ untrackedHubModels.length}
+ )
+
+
+ {!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/CameraBindings.tsx b/src/components/landing/CameraBindings.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d9b9723aa3f73bac268f9cdceee8e2d235c1049f
--- /dev/null
+++ b/src/components/landing/CameraBindings.tsx
@@ -0,0 +1,92 @@
+import React from "react";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { AvailableCamera } from "@/hooks/useAvailableCameras";
+import { ImageFeatures } from "@/lib/checkpointsApi";
+import BrowserCameraPreview from "@/components/control/BrowserCameraPreview";
+import CameraRotationSelect from "@/components/control/CameraRotationSelect";
+import { CameraRotation, rotatedDimensions } from "@/lib/cameraConfig";
+
+interface CameraBindingsProps {
+ imageFeatures: ImageFeatures;
+ bindings: Record;
+ onBind: (name: string, index: number) => void;
+ rotationFor: (name: string) => CameraRotation;
+ onRotate: (name: string, rotation: CameraRotation) => void;
+ availableCameras: AvailableCamera[];
+ /** Drop the previews so the backend can open the same devices. */
+ paused: boolean;
+}
+
+/** One row per expected camera: its resolution, a physical camera picker and a preview. */
+const CameraBindings: React.FC = ({
+ imageFeatures,
+ bindings,
+ onBind,
+ rotationFor,
+ onRotate,
+ availableCameras,
+ paused,
+}) => (
+ <>
+ {Object.entries(imageFeatures).map(([name, dims]) => {
+ const value = bindings[name];
+ const bound =
+ value != null ? availableCameras.find((c) => c.index === value) : undefined;
+ const rotation = rotationFor(name);
+ const capture = rotatedDimensions(dims.width, dims.height, rotation);
+ return (
+
+
+
{name}
+
+ {dims.width}×{dims.height}
+
+
+
onBind(name, Number(v))}
+ >
+
+
+
+
+ {availableCameras.length === 0 ? (
+
+ No cameras detected
+
+ ) : (
+ availableCameras.map((cam) => (
+
+ #{cam.index} — {cam.name}
+
+ ))
+ )}
+
+
+
onRotate(name, angle)}
+ />
+
+
+ );
+ })}
+ >
+);
+
+export default CameraBindings;
diff --git a/src/components/landing/DatasetPicker.tsx b/src/components/landing/DatasetPicker.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..14822e830b5f6f69afcf53cb9cc93c5524e6741b
--- /dev/null
+++ b/src/components/landing/DatasetPicker.tsx
@@ -0,0 +1,181 @@
+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 = !!onCreateNew && trimmed.length > 0 && isName && !matchesExisting;
+ const canOpenCustom = !!onOpenCustom && 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…"
+ : onCreateNew
+ ? "No datasets yet. Type a name to create one."
+ : "No datasets available."}
+
+ )}
+ {localDatasets.length > 0 && (
+
+ {localDatasets.map(renderItem)}
+
+ )}
+ {hubDatasets.length > 0 && (
+
+ {hubDatasets.map(renderItem)}
+
+ )}
+ {canOpenCustom && (
+
+
+
+ Open "{trimmed}" in viewer
+
+
+ )}
+
+ {onCreateNew && (
+
+
+ {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.test.tsx b/src/components/landing/InferenceModal.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..313cbb2a2165ca8e3b4bc88ea22ba330582159f3
--- /dev/null
+++ b/src/components/landing/InferenceModal.test.tsx
@@ -0,0 +1,111 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot, Root } from "react-dom/client";
+import { afterEach, beforeEach, expect, test, vi } from "vitest";
+import InferenceModal from "./InferenceModal";
+import { RobotRecord } from "@/hooks/useRobots";
+const mocks = vi.hoisted(() => ({ fetcher: vi.fn(), navigate: vi.fn(), toast: vi.fn() }));
+const available = [
+ { index: 1, name: "top", deviceId: "usb-top", available: true },
+ { index: 2, name: "wrist", deviceId: "usb-wrist", available: true },
+ { index: 3, name: "manual", deviceId: "usb-manual", available: true },
+];
+vi.mock("@/contexts/ApiContext", () => ({ useApi: () => ({ baseUrl: "http://test", fetchWithHeaders: mocks.fetcher }) }));
+vi.mock("@/hooks/use-toast", () => ({ useToast: () => ({ toast: mocks.toast }) }));
+vi.mock("react-router-dom", () => ({ useNavigate: () => mocks.navigate }));
+vi.mock("@/hooks/useAvailableCameras", () => ({ useAvailableCameras: () => ({ cameras: available }) }));
+vi.mock("@/hooks/useCameraStream", () => ({ useCameraStream: () => ({ videoRef: React.createRef(), hasError: false }) }));
+// Use native selectors to test modal state changes without Radix popup event details.
+vi.mock("@/components/ui/select", () => ({
+ Select: ({ value, onValueChange, disabled, children }: { value: string; onValueChange: (value: string) => void; disabled: boolean; children: React.ReactNode }) => (
+ onValueChange(event.target.value)}> {children}
+ ),
+ SelectContent: ({ children }: { children: React.ReactNode }) => <>{children}>,
+ SelectItem: ({ value, children }: { value: string; children: React.ReactNode }) => {children} ,
+ SelectTrigger: () => null,
+ SelectValue: () => null,
+}));
+vi.mock("@/components/ui/dialog", () => ({
+ Dialog: ({ open, children }: { open: boolean; children: React.ReactNode }) => open ? {children}
: null,
+ DialogContent: ({ children }: { children: React.ReactNode }) => {children}
,
+ DialogHeader: ({ children }: { children: React.ReactNode }) => {children}
,
+ DialogTitle: ({ children }: { children: React.ReactNode }) => {children} ,
+ DialogDescription: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+const robot: RobotRecord = {
+ name: "so101", leader_port: "leader", follower_port: "follower", leader_config: "leader.json", follower_config: "follower.json", is_clean: true,
+ cameras: [
+ { id: "wrist", name: "wrist", type: "opencv", device_id: "usb-wrist", camera_index: 1, width: 640, height: 480, rotation: 90 },
+ { id: "top", name: "top", type: "opencv", device_id: "usb-top", camera_index: 2, width: 640, height: 480, rotation: 180 },
+ ],
+};
+let root: Root;
+let container: HTMLDivElement;
+beforeEach(() => {
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
+ container = document.createElement("div"); root = createRoot(container);
+ mocks.fetcher.mockReset(); mocks.navigate.mockReset(); mocks.toast.mockReset();
+ mocks.fetcher.mockImplementation(async (url: string) => new Response(JSON.stringify(
+ url.endsWith("/checkpoints") ? { checkpoints: [{ step: 10, source: "local", ref: "checkpoint" }] }
+ : url.endsWith("/policy-config") ? { policy_type: "act", requires_task: false, image_features: { wrist: { width: 480, height: 640 }, top: { width: 640, height: 480 } } }
+ : { message: "Started", log_path: "rollout.log" },
+ )));
+});
+afterEach(() => { act(() => root.unmount()); vi.useRealTimers(); });
+const rotations = () => Array.from(container.querySelectorAll("select")).filter((select) => Array.from(select.options).some((option) => option.text.includes("clockwise")));
+const bindings = () => Array.from(container.querySelectorAll("select")).filter((select) => Array.from(select.options).some((option) => option.text.includes("#")));
+function change(select: HTMLSelectElement, value: string) {
+ act(() => { select.value = value; select.dispatchEvent(new Event("change", { bubbles: true })); });
+}
+async function render(record = robot) {
+ await act(async () => root.render( ));
+}
+async function start() {
+ vi.useFakeTimers();
+ const button = Array.from(container.querySelectorAll("button")).find((item) => item.textContent?.includes("Start Inference"))!;
+ await act(async () => { button.click(); await vi.advanceTimersByTimeAsync(300); });
+ const call = mocks.fetcher.mock.calls.find(([url]) => url.endsWith("/start-inference"));
+ return call ? JSON.parse(call[1].body) : null;
+}
+
+test("camera reassignment carries physical rotation and sends native dimensions to inference", async () => {
+ await render();
+ expect(bindings().map((select) => select.value)).toEqual(["2", "1"]);
+ expect(rotations().map((select) => select.value)).toEqual(["90", "180"]);
+ change(bindings()[0], "1"); change(bindings()[1], "2");
+ expect(rotations().map((select) => select.value)).toEqual(["180", "90"]);
+ change(rotations()[0], "270");
+ const request = await start();
+ expect(request.cameras.wrist).toMatchObject({ camera_index: 1, rotation: 270, width: 640, height: 480 });
+ expect(request.cameras.top).toMatchObject({ camera_index: 2, rotation: 90, width: 480, height: 640 });
+ expect(mocks.navigate).toHaveBeenCalledWith("/inference");
+});
+
+test("an unsaved camera permits a manual rotation with a training-data reminder", async () => {
+ await render(); change(bindings()[0], "3");
+ expect(rotations()[0].value).toBe("0");
+ change(rotations()[0], "90");
+ expect(container.textContent).toContain("Use the same rotation as the training data");
+ const request = await start();
+ expect(request.cameras.wrist).toMatchObject({ camera_index: 3, rotation: 90, width: 640, height: 480 });
+});
+
+test("the plugin package note shows only for a policy type LeRobot does not register", async () => {
+ await render();
+ expect(container.textContent).not.toContain("plugin policy");
+ act(() => root.unmount()); root = createRoot(container);
+ const builtin = mocks.fetcher.getMockImplementation()!;
+ mocks.fetcher.mockImplementation(async (url: string, init?: RequestInit) => url.endsWith("/policy-config")
+ ? new Response(JSON.stringify({ policy_type: "my_plugin", requires_task: true, image_features: {} }))
+ : builtin(url, init));
+ await render();
+ expect(container.textContent).toContain("This is a plugin policy");
+});
+
+test("an invalid saved rotation blocks inference and gives a visible correction instruction", async () => {
+ await render({ ...robot, cameras: [{ ...robot.cameras[0], rotation: 45 as never }] });
+ expect(container.textContent).toContain("Correct the camera settings in Calibration");
+ const button = Array.from(container.querySelectorAll("button")).find((item) => item.textContent?.includes("Start Inference"))! as HTMLButtonElement;
+ expect(button.disabled).toBe(true);
+ expect(await start()).toBeNull();
+});
diff --git a/src/components/landing/InferenceModal.tsx b/src/components/landing/InferenceModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d7b3ad031ded091db744696cdce818b64d6cf73a
--- /dev/null
+++ b/src/components/landing/InferenceModal.tsx
@@ -0,0 +1,362 @@
+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 { AlertTriangle, CheckCircle, Loader2, Play } 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 { useCameraBindings } from "@/hooks/useCameraBindings";
+import { cameraConfigurationError } from "@/lib/cameraConfig";
+import CameraBindings from "./CameraBindings";
+
+interface Props {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ robot: RobotRecord | null;
+ jobId: string;
+ initialStep: number | null;
+}
+
+const DEFAULT_FPS = 30;
+// Policy types LeRobot (v0.6.1) registers itself; any other type comes from a plugin package.
+const BUILTIN_POLICY_TYPES = new Set([
+ "act",
+ "diffusion",
+ "eo1",
+ "evo1",
+ "fastwam",
+ "gaussian_actor",
+ "groot",
+ "lingbot_va",
+ "molmoact2",
+ "multi_task_dit",
+ "pi0",
+ "pi05",
+ "pi0_fast",
+ "smolvla",
+ "tdmpc",
+ "vla_jepa",
+ "vqbet",
+ "wall_x",
+ "xvla",
+]);
+
+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);
+
+ const cameraBindings = useCameraBindings(policyConfig?.image_features ?? null, robot, 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);
+ })
+ .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]);
+
+ const selectedRef =
+ selectedStep != null
+ ? checkpoints.find((c) => c.step === selectedStep)?.ref ?? null
+ : null;
+
+ const cameraError = robot ? cameraConfigurationError(robot.cameras ?? []) : null;
+
+ const canStart =
+ !!robot &&
+ robot.is_clean &&
+ !cameraError &&
+ selectedRef != null &&
+ !!policyConfig &&
+ cameraBindings.allBound &&
+ !submitting;
+
+ const handleStart = async () => {
+ if (!robot || selectedRef == null || !policyConfig || cameraError) 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));
+ try {
+ await startInference(baseUrl, fetchWithHeaders, {
+ follower_port: robot.follower_port,
+ follower_config: robot.follower_config,
+ policy_ref: selectedRef,
+ task,
+ cameras: cameraBindings.toCameraDict(DEFAULT_FPS),
+ 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);
+ }
+ };
+
+ return (
+
+
+
+
+
+ Configure Inference
+
+
+
+
+
+ Pick a checkpoint and confirm hardware. The selected policy will
+ drive the follower autonomously for the configured duration.
+
+ {policyConfig && !BUILTIN_POLICY_TYPES.has(policyConfig.policy_type ?? "") && (
+
+ This is a plugin policy: its package must be installed alongside
+ lelab, or the run will fail to load the policy.
+
+ )}
+
+
+
+ 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
+
+ {cameraError ? (
+
+
+ {cameraError} Correct the camera settings in Calibration.
+
+ ) : policyConfigLoading ? (
+
+
+ Reading policy config…
+
+ ) : policyConfigError ? (
+
+
+
+ Couldn't load policy config: {policyConfigError}
+
+
+ ) : !policyConfig ? null : Object.keys(policyConfig.image_features).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. Use the same
+ rotation as the training data.
+
+
+
+ )}
+
+
+
+
+
+ {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..a36321f62ee47908771262623756941063ac9e4c
--- /dev/null
+++ b/src/components/landing/LandingTopBar.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import HfAuthChip from "./HfAuthChip";
+import TourLauncher from "@/components/onboarding/TourLauncher";
+
+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..0cff4ee1a7610ea7641758bb9270ff2ebdb5400d
--- /dev/null
+++ b/src/components/landing/RecordingModal.tsx
@@ -0,0 +1,351 @@
+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 { cameraConfigurationError } from "@/lib/cameraConfig";
+import CameraConfiguration, {
+ CameraConfig,
+} from "@/components/recording/CameraConfiguration";
+import { useHfAuth } from "@/contexts/HfAuthContext";
+import { RobotRecord } from "@/hooks/useRobots";
+import { AppendTarget } from "@/hooks/useRecording";
+import { useCameraBindings } from "@/hooks/useCameraBindings";
+import CameraBindings from "./CameraBindings";
+
+interface RecordingModalProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ robot: RobotRecord | null;
+ appendTo: AppendTarget | null;
+ cameraBindings: ReturnType;
+ 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,
+ appendTo,
+ cameraBindings,
+ 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 && (!appendTo || cameraBindings.allBound);
+
+ return (
+
+
+
+
+
+ {appendTo ? "Record More Episodes" : "Configure Recording"}
+
+
+
+
+ {appendTo ? (
+ <>
+ New episodes are added to{" "}
+ {appendTo.repoId}
+ , which has {appendTo.numEpisodes} so far.
+ >
+ ) : (
+ "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
+
+
+ {!appendTo && (
+
+
+ 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"
+ />
+
+
+
+ {appendTo ? "Episodes to Add" : "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"
+ />
+
+
+
+
+
+ {appendTo ? (
+ Object.keys(appendTo.imageFeatures).length > 0 &&
+ !(robot && cameraConfigurationError(robot.cameras ?? [])) && (
+
+
+ Cameras
+
+
+ Bind a physical camera to each camera the dataset was
+ recorded with. Resolution comes from the dataset.
+
+
+
+ )
+ ) : (
+
+
+
+ These are the cameras set up for this robot. To add or change a
+ camera, configure it on the Calibration page. Save a reference
+ frame to see it as a ghost over the live preview and realign
+ the rig next session.
+
+
+ )}
+
+
+
+ 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..fed18c4dbd09bef3ecee267e1ff4bca2a29d501f
--- /dev/null
+++ b/src/components/landing/RobotConfigManager.tsx
@@ -0,0 +1,88 @@
+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();
+ // The backend returns HTTP 200 with `{ success: false }` for logical
+ // failures (arm not connected, already active), so gate on `data.success`
+ // — not just `res.ok` — or we'd navigate to an empty teleop screen.
+ if (res.ok && data.success) {
+ 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..f8790c985ad15304914cf5e1ff7c10c18f1bcc78
--- /dev/null
+++ b/src/components/landing/RobotTile.tsx
@@ -0,0 +1,161 @@
+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"
+ data-tour="robot-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..362aa60f34ffb644b680be63a67b1d9feca88bb9
--- /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 --with-executables-from huggingface_hub 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/onboarding/SpotlightOverlay.tsx b/src/components/onboarding/SpotlightOverlay.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e5c3d9d3b6615acb96c47a6b359dac87816dfccf
--- /dev/null
+++ b/src/components/onboarding/SpotlightOverlay.tsx
@@ -0,0 +1,129 @@
+import React, { useEffect, useLayoutEffect, useState } from "react";
+import { createPortal } from "react-dom";
+import { useLocation } from "react-router-dom";
+import { useOnboarding } from "@/contexts/OnboardingContext";
+import TourCard from "@/components/onboarding/TourCard";
+
+// Padding around the highlighted element, in px.
+const PAD = 8;
+// How long to wait for a step's target to appear (e.g. after a route change)
+// before falling back to a centered card so the tour never blocks.
+const MOUNT_TIMEOUT_MS = 3000;
+
+const SpotlightOverlay: React.FC = () => {
+ const { isActive, currentStep, stop } = useOnboarding();
+ const location = useLocation();
+ const [rect, setRect] = useState(null);
+ // Becomes true once we've either located the target or given up (fallback).
+ const [ready, setReady] = useState(false);
+
+ const stepId = currentStep?.id ?? null;
+ const target = currentStep?.target ?? null;
+
+ // Locate the step's target, tracking it across scroll/resize/layout shifts.
+ // Re-runs whenever the step changes, and on route changes so a manual
+ // navigation away mid-step re-queries (rather than pinning a stale rect).
+ useLayoutEffect(() => {
+ setReady(false);
+ setRect(null);
+ if (!isActive || !stepId) return;
+
+ // Centered steps (welcome/done) have no target.
+ if (!target) {
+ setReady(true);
+ return;
+ }
+
+ let raf = 0;
+ let observer: ResizeObserver | null = null;
+ let tracked: Element | null = null;
+ const startedAt = performance.now();
+
+ const measure = () => {
+ if (!tracked) return;
+ // The element can leave the DOM under us (e.g. the user navigated away
+ // before this effect re-runs). Drop the spotlight instead of pinning it
+ // to a stale position.
+ if (!tracked.isConnected) {
+ setRect(null);
+ return;
+ }
+ setRect(tracked.getBoundingClientRect());
+ };
+
+ const attachTrackers = (el: Element) => {
+ tracked = el;
+ // Bring the target on-screen if it's below the fold (e.g. the cameras or
+ // jobs step). "nearest" scrolls the minimum needed and does nothing when
+ // it's already visible; the scroll listener below keeps the cutout and
+ // card glued to it as the smooth scroll settles.
+ el.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" });
+ measure();
+ setReady(true);
+ window.addEventListener("scroll", measure, true);
+ window.addEventListener("resize", measure);
+ observer = new ResizeObserver(measure);
+ observer.observe(el);
+ };
+
+ const poll = () => {
+ const el = document.querySelector(`[data-tour="${target}"]`);
+ if (el) {
+ attachTrackers(el);
+ return;
+ }
+ if (performance.now() - startedAt > MOUNT_TIMEOUT_MS) {
+ // Target never mounted — show the copy centered rather than block.
+ setRect(null);
+ setReady(true);
+ return;
+ }
+ raf = requestAnimationFrame(poll);
+ };
+ poll();
+
+ return () => {
+ if (raf) cancelAnimationFrame(raf);
+ window.removeEventListener("scroll", measure, true);
+ window.removeEventListener("resize", measure);
+ observer?.disconnect();
+ };
+ }, [isActive, stepId, target, location.pathname]);
+
+ // Esc leaves the tour from anywhere.
+ useEffect(() => {
+ if (!isActive) return;
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape") stop(false);
+ };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [isActive, stop]);
+
+ if (!isActive || !currentStep || !ready) return null;
+
+ const hasRect = !!rect && rect.width > 0 && rect.height > 0;
+
+ return createPortal(
+
+ {hasRect ? (
+
+ ) : (
+
+ )}
+
+
,
+ document.body
+ );
+};
+
+export default SpotlightOverlay;
diff --git a/src/components/onboarding/TourCard.tsx b/src/components/onboarding/TourCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f2df23094b66d281c8e8215ca03c0d34bf669905
--- /dev/null
+++ b/src/components/onboarding/TourCard.tsx
@@ -0,0 +1,185 @@
+import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
+import { Check, Lock, X } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import { useOnboarding } from "@/contexts/OnboardingContext";
+
+const MARGIN = 8;
+const GAP = 12;
+const CARD_W = 340;
+
+const clamp = (v: number, lo: number, hi: number) =>
+ Math.max(lo, Math.min(hi, v));
+
+interface TourCardProps {
+ /** Bounding rect of the highlighted element, or null for a centered card. */
+ rect: DOMRect | null;
+}
+
+const TourCard: React.FC = ({ rect }) => {
+ const {
+ currentStep,
+ stepIndex,
+ totalSteps,
+ currentComplete,
+ currentGated,
+ next,
+ back,
+ skipStep,
+ stop,
+ } = useOnboarding();
+ const cardRef = useRef(null);
+ const [size, setSize] = useState({ w: CARD_W, h: 0 });
+
+ useLayoutEffect(() => {
+ if (cardRef.current) {
+ const r = cardRef.current.getBoundingClientRect();
+ setSize({ w: r.width, h: r.height });
+ }
+ }, [currentStep?.id, rect]);
+
+ // Move focus to the card when a step opens so keyboard and screen-reader
+ // users land on the guidance. The card remounts per step (keyed by id), so
+ // this fires once per step.
+ useEffect(() => {
+ cardRef.current?.focus();
+ }, []);
+
+ if (!currentStep) return null;
+
+ const placement = currentStep.placement ?? "bottom";
+ const centered = !rect || placement === "center";
+ const isLast = stepIndex >= totalSteps - 1;
+ const isFirst = stepIndex <= 0;
+
+ let pos: React.CSSProperties;
+ if (centered) {
+ pos = { top: "50%", left: "50%", transform: "translate(-50%, -50%)" };
+ } else {
+ const vw = window.innerWidth;
+ const vh = window.innerHeight;
+ let top = 0;
+ let left = 0;
+ switch (placement) {
+ case "top":
+ top = rect!.top - size.h - GAP;
+ left = rect!.left + rect!.width / 2 - size.w / 2;
+ break;
+ case "left":
+ left = rect!.left - size.w - GAP;
+ top = rect!.top + rect!.height / 2 - size.h / 2;
+ break;
+ case "right":
+ left = rect!.right + GAP;
+ top = rect!.top + rect!.height / 2 - size.h / 2;
+ break;
+ case "bottom":
+ default:
+ top = rect!.bottom + GAP;
+ left = rect!.left + rect!.width / 2 - size.w / 2;
+ break;
+ }
+ // Flip to the opposite side if the preferred one overflows the viewport.
+ if (placement === "bottom" && top + size.h > vh - MARGIN)
+ top = rect!.top - size.h - GAP;
+ if (placement === "top" && top < MARGIN) top = rect!.bottom + GAP;
+ if (placement === "right" && left + size.w > vw - MARGIN)
+ left = rect!.left - size.w - GAP;
+ if (placement === "left" && left < MARGIN) left = rect!.right + GAP;
+ // Keep the card fully on screen.
+ left = clamp(left, MARGIN, vw - size.w - MARGIN);
+ top = clamp(top, MARGIN, vh - size.h - MARGIN);
+ pos = { top, left };
+ }
+
+ return (
+
+
stop(false)}
+ aria-label="Exit tour"
+ className="absolute right-2 top-2 rounded p-1 text-gray-400 hover:bg-gray-800 hover:text-white"
+ >
+
+
+
+
+
+
+ Step {stepIndex + 1} of {totalSteps}
+
+ {currentComplete && (
+
+
+ Done
+
+ )}
+
+
+ {currentStep.title}
+
+
+ {currentStep.body}
+
+
+ {currentGated && !currentComplete && (
+
+
+ Finish the earlier step to unlock this, or continue with Next
+ whenever you like.
+
+ )}
+
+
+
+ Back
+
+
+ {currentStep.optional && !isLast && (
+
+ Skip
+
+ )}
+
+ {isLast ? "Finish" : currentComplete ? "Continue" : "Next"}
+
+
+
+
+
+ );
+};
+
+export default TourCard;
diff --git a/src/components/onboarding/TourLauncher.tsx b/src/components/onboarding/TourLauncher.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..041d059eb459bfea0be5a4f3e864baf7c8b3151d
--- /dev/null
+++ b/src/components/onboarding/TourLauncher.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { HelpCircle } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { useOnboarding } from "@/contexts/OnboardingContext";
+
+interface TourLauncherProps {
+ /** "floating" pins a round button to the corner on every page; "inline" is
+ * a compact button for the landing top bar. */
+ variant?: "floating" | "inline";
+ className?: string;
+}
+
+const TourLauncher: React.FC = ({
+ variant = "inline",
+ className,
+}) => {
+ const { start, isActive } = useOnboarding();
+
+ // The floating button would only get in the way while the tour is running.
+ if (variant === "floating" && isActive) return null;
+
+ if (variant === "floating") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ Tour
+
+ );
+};
+
+export default TourLauncher;
diff --git a/src/components/onboarding/WelcomeDialog.tsx b/src/components/onboarding/WelcomeDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..17c839f41d2f4405bd68a689ded6e64bb5ee462f
--- /dev/null
+++ b/src/components/onboarding/WelcomeDialog.tsx
@@ -0,0 +1,77 @@
+import React from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Compass, Route } from "lucide-react";
+
+interface WelcomeDialogProps {
+ open: boolean;
+ onStart: () => void;
+ onDismiss: () => void;
+}
+
+// The stages a newcomer will walk through, shown as a quick preview so the
+// tour's value is clear before they commit two minutes to it.
+const STAGES = ["Calibrate", "Record", "Train", "Run"];
+
+const WelcomeDialog: React.FC = ({
+ open,
+ onStart,
+ onDismiss,
+}) => {
+ return (
+ (next ? undefined : onDismiss())}>
+
+
+
+
+ Welcome to LeLab
+
+
+ New to teaching a robot arm? A two-minute guided tour walks you
+ through the whole loop, no jargon required.
+
+
+
+
+
+ {STAGES.map((stage, i) => (
+
+
+ {stage}
+
+ {i < STAGES.length - 1 && (
+ →
+ )}
+
+ ))}
+
+
+
+
+
+
+ Take the tour
+
+
+ Maybe later
+
+
+
+
+ );
+};
+
+export default WelcomeDialog;
diff --git a/src/components/recording/CameraConfiguration.test.tsx b/src/components/recording/CameraConfiguration.test.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..571552e9ce0849c38d333541489311a48517dda9
--- /dev/null
+++ b/src/components/recording/CameraConfiguration.test.tsx
@@ -0,0 +1,61 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot, Root } from "react-dom/client";
+import { afterEach, beforeEach, expect, test, vi } from "vitest";
+import CameraConfiguration from "./CameraConfiguration";
+import { CameraConfig, normalizeCameraConfig, serializeCameras } from "@/lib/cameraConfig";
+vi.mock("@/hooks/useAvailableCameras", () => ({ useAvailableCameras: () => ({ cameras: [], isLoading: false, refresh: vi.fn() }) }));
+vi.mock("@/hooks/use-toast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
+const reference = { url: null as string | null, error: null, save: vi.fn(), clear: vi.fn() };
+vi.mock("@/hooks/useCameraReference", () => ({ useCameraReference: () => reference }));
+vi.mock("@/hooks/useCameraStream", () => ({ useCameraStream: () => ({ videoRef: React.createRef(), hasError: false }) }));
+let root: Root;
+let container: HTMLDivElement;
+const camera: CameraConfig = { id: "wrist", name: "wrist", device_id: "usb-wrist", type: "opencv", width: 640, height: 480 };
+beforeEach(() => { Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); container = document.createElement("div"); root = createRoot(container); });
+afterEach(() => act(() => root.unmount()));
+test("rotation updates the profile while capture dimensions stay native", () => {
+ const change = vi.fn();
+ act(() => root.render( ));
+ const select = container.querySelector("select")!;
+ expect(select.value).toBe("0");
+ act(() => { select.value = "90"; select.dispatchEvent(new Event("change", { bubbles: true })); });
+ expect(change).toHaveBeenCalledWith([{ ...camera, rotation: 90 }]);
+ const saved = JSON.parse(JSON.stringify(change.mock.calls[0][0])).map(normalizeCameraConfig);
+ act(() => root.render( ));
+ expect(container.querySelector("select")!.value).toBe("90");
+ expect(container.querySelector("video")!.style.transform).toContain("rotate(90deg)");
+ expect(serializeCameras(saved).wrist).toMatchObject({ rotation: 90, width: 640, height: 480 });
+});
+test("the recording configuration displays rotated output without an editing control", () => {
+ act(() => root.render( ));
+ expect(container.textContent).toContain("480×640 · 90° clockwise");
+ expect(container.querySelector("select")).toBeNull();
+ expect(container.querySelector("video")!.parentElement!.style.aspectRatio).toBe("480 / 640");
+});
+
+test("an invalid stored rotation stays visible until the user selects a valid value", () => {
+ const change = vi.fn();
+ const invalid = { ...camera, rotation: 45 as never };
+ act(() => root.render( ));
+ expect(container.querySelector("[role=alert]")!.textContent).toContain("Select a valid rotation");
+ expect(container.querySelector("video")).toBeNull();
+ const select = container.querySelector("select")!;
+ expect(select.value).toBe("");
+ act(() => { select.value = "180"; select.dispatchEvent(new Event("change", { bubbles: true })); });
+ expect(change).toHaveBeenCalledWith([{ ...camera, rotation: 180 }]);
+});
+test("reference controls appear only for a named robot in read-only mode", () => {
+ act(() => root.render( ));
+ expect(container.textContent).not.toContain("Set reference");
+ act(() => root.render( ));
+ expect(container.textContent).toContain("Set reference");
+ expect(container.querySelector('input[type="range"]')).toBeNull();
+ reference.url = "blob:ref";
+ act(() => root.render( ));
+ expect(container.textContent).toContain("Replace reference");
+ expect(container.querySelector("img")!.getAttribute("src")).toBe("blob:ref");
+ const slider = container.querySelector('input[type="range"]')!;
+ expect(slider).not.toBeNull();
+ reference.url = null;
+});
diff --git a/src/components/recording/CameraConfiguration.tsx b/src/components/recording/CameraConfiguration.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..60c6b6907e83fe0c12e3b8e8aae250abfdf6cb98
--- /dev/null
+++ b/src/components/recording/CameraConfiguration.tsx
@@ -0,0 +1,538 @@
+import React, { useState, useEffect, useCallback, useRef } 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, RefreshCw, ChevronRight } from "lucide-react";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { useToast } from "@/hooks/use-toast";
+import { useAvailableCameras } from "@/hooks/useAvailableCameras";
+import { useCameraReference } from "@/hooks/useCameraReference";
+import BrowserCameraPreview from "@/components/control/BrowserCameraPreview";
+import CameraRotationSelect from "@/components/control/CameraRotationSelect";
+import CameraReferenceControls from "@/components/recording/CameraReferenceControls";
+import {
+ CameraConfig,
+ cameraRotation,
+ cameraRotationError,
+ previewDeviceId,
+ rotatedDimensions,
+} from "@/lib/cameraConfig";
+export type { CameraConfig } from "@/lib/cameraConfig";
+
+// Sentinels distinguish "leave unset" (auto-detect / platform default) from an
+// explicit choice. Radix Select disallows an empty-string value, so we map these
+// to `undefined` on the CameraConfig.
+const FOURCC_AUTO = "__auto__";
+const BACKEND_DEFAULT = "__default__";
+const FOURCC_OPTIONS = ["MJPG", "YUYV", "I420", "NV12", "H264", "MP4V"];
+// Mirrors lerobot's Cv2Backends enum names.
+const BACKEND_OPTIONS = [
+ "ANY",
+ "V4L2",
+ "DSHOW",
+ "PVAPI",
+ "ANDROID",
+ "AVFOUNDATION",
+ "MSMF",
+];
+
+interface CameraConfigurationProps {
+ cameras: CameraConfig[];
+ onCamerasChange: (cameras: CameraConfig[]) => void;
+ releaseStreamsRef?: React.MutableRefObject<(() => void) | null>; // Ref to expose stream release function
+ // When true, hide the add/remove/edit controls and show the configured
+ // cameras as read-only live previews. Used in the recording dialog, where
+ // cameras are managed on the Calibration page rather than added ad hoc.
+ readOnly?: boolean;
+ // Robot the cameras belong to. In read-only mode this enables the reference
+ // frame controls, which are stored per robot and camera.
+ robotName?: string;
+}
+
+const CameraConfiguration: React.FC = ({
+ cameras,
+ onCamerasChange,
+ releaseStreamsRef,
+ readOnly = false,
+ robotName,
+}) => {
+ const { toast } = useToast();
+
+ const {
+ cameras: availableCameras,
+ isLoading: isLoadingCameras,
+ refresh: refreshCameras,
+ } = 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,
+ rotation: 0,
+ };
+
+ 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 (
+
+
+ {readOnly ? "Cameras" : "Camera Configuration"}
+
+
+ {/* Add Camera Section — hidden in read-only mode (cameras are managed on
+ the Calibration page, not added here). */}
+ {!readOnly && (
+
+
Add Camera
+
+
+
+
+
+ Available Cameras
+
+ refreshCameras()}
+ disabled={isLoadingCameras}
+ className="h-6 w-6 text-gray-400 hover:text-white"
+ title="Rescan for cameras (e.g. after plugging in a new USB camera)"
+ aria-label="Rescan for 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 && (
+
+ {!readOnly && (
+
+ Configured Cameras ({cameras.length})
+
+ )}
+
+
+ {cameras.map((camera) => (
+ removeCamera(camera.id)}
+ onUpdate={(updates) => updateCamera(camera.id, updates)}
+ />
+ ))}
+
+
+ )}
+
+ {cameras.length === 0 && (
+
+
+
+ {readOnly
+ ? "No cameras are configured for this robot."
+ : "No cameras configured. Add a camera to get started."}
+
+
+ )}
+
+ );
+};
+
+interface CameraPreviewProps {
+ camera: CameraConfig;
+ deviceId: string;
+ paused: boolean;
+ readOnly: boolean;
+ robotName?: string;
+ onRemove: () => void;
+ onUpdate: (updates: Partial) => void;
+}
+
+const CameraPreview: React.FC = ({
+ camera,
+ deviceId,
+ paused,
+ readOnly,
+ robotName,
+ onRemove,
+ onUpdate,
+}) => {
+ const previewRef = useRef(null);
+ const [ghostOpacity, setGhostOpacity] = useState(0.5);
+ const reference = useCameraReference(readOnly ? robotName : undefined, camera.name);
+ const rotationError = cameraRotationError(camera);
+ const rotation = rotationError ? null : cameraRotation(camera.rotation);
+ const output = rotation === null ? null : rotatedDimensions(camera.width, camera.height, rotation);
+ return (
+
+
+
+
+
+ {/* Camera Info */}
+
+
+
{camera.name}
+ {!readOnly && (
+
+
+
+ )}
+
+
+ {readOnly && robotName && (
+
{
+ const video = previewRef.current?.querySelector("video");
+ if (video) void reference.save(video);
+ }}
+ onClear={() => void reference.clear()}
+ disabled={paused || !!rotationError}
+ error={reference.error}
+ />
+ )}
+ {rotationError && {rotationError} Select a valid rotation.
}
+ {readOnly ? (
+
+ {output ? `${output.width}×${output.height} · ${rotation}° clockwise` : "Invalid rotation"}
+ {camera.fps ? ` · ${camera.fps} fps` : ""}
+
+ ) : (
+ <>
+ onUpdate({ rotation: value })} />
+
+
+
+ Configuration
+
+
+
+
+
Capture:
+
+ {
+ 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"
+ />
+
+
+ FOURCC:
+
+ onUpdate({ fourcc: v === FOURCC_AUTO ? undefined : v })
+ }
+ >
+
+
+
+
+
+ Auto
+
+ {FOURCC_OPTIONS.map((code) => (
+
+ {code}
+
+ ))}
+
+
+
+
+ Backend:
+
+ onUpdate({ backend: v === BACKEND_DEFAULT ? undefined : v })
+ }
+ >
+
+
+
+
+
+ Default
+
+ {BACKEND_OPTIONS.map((name) => (
+
+ {name}
+
+ ))}
+
+
+
+
+ Overriding the backend can reorder camera indices on macOS.
+
+
+
+ Type: {camera.type} | Device:{" "}
+ {camera.device_id?.substring(0, 10)}...
+
+
+
+ >
+ )}
+
+
+ );
+};
+
+export default CameraConfiguration;
diff --git a/src/components/recording/CameraReferenceControls.tsx b/src/components/recording/CameraReferenceControls.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c23e97acca744ef947cdf88decfb1e6996a09631
--- /dev/null
+++ b/src/components/recording/CameraReferenceControls.tsx
@@ -0,0 +1,67 @@
+import React from "react";
+import { Button } from "@/components/ui/button";
+
+interface Props {
+ hasReference: boolean;
+ opacity: number;
+ onOpacityChange: (opacity: number) => void;
+ onSave: () => void;
+ onClear: () => void;
+ disabled: boolean;
+ error: string | null;
+}
+
+/** Save the current frame as the reference, then slide the ghost overlay over the live preview. */
+const CameraReferenceControls: React.FC = ({
+ hasReference,
+ opacity,
+ onOpacityChange,
+ onSave,
+ onClear,
+ disabled,
+ error,
+}) => (
+
+
+
+ {hasReference ? "Replace reference" : "Set reference"}
+
+ {hasReference && (
+
+ Clear
+
+ )}
+
+ {hasReference && (
+
+ Ghost
+ onOpacityChange(Number(e.target.value))}
+ className="flex-1"
+ aria-label="Reference overlay opacity"
+ />
+
+ )}
+ {error &&
{error}
}
+
+);
+
+export default CameraReferenceControls;
diff --git a/src/components/replay/DatasetCombobox.tsx b/src/components/replay/DatasetCombobox.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4e5fdb06f68197c1fffb26824eff60a5591eb3c2
--- /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..caa0ab8cc801c0a153c2cdd31a2e9887403392e3
--- /dev/null
+++ b/src/components/training/ConfigurationTab.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import EssentialsCard from './config/EssentialsCard';
+import GrootCard from './config/GrootCard';
+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/PolicyExtraDialog.tsx b/src/components/training/PolicyExtraDialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ff97982a2349ccf10f84aaf406bac8f16bc12972
--- /dev/null
+++ b/src/components/training/PolicyExtraDialog.tsx
@@ -0,0 +1,81 @@
+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;
+ policyType: string;
+ packageName: string; // the probed module, e.g. "transformers"
+ installTarget: string; // e.g. "lerobot[smolvla]"
+ installHint: string; // e.g. "pip install 'lerobot[smolvla]'"
+}
+
+// Some policies (smolvla, pi0, pi0_fast, diffusion) need an optional LeRobot
+// extra. This catches the missing package before training starts and offers a
+// one-click install, instead of the run dying with a buried ImportError.
+const PolicyExtraDialog: React.FC = ({
+ open,
+ onOpenChange,
+ policyType,
+ packageName,
+ installTarget,
+ installHint,
+}) => {
+ const install = useInstallExtra(`system/policy-extra/${policyType}`, open);
+ const title = `${policyType.toUpperCase()} needs an extra package`;
+
+ return (
+
+
+
+
+
+ {installTitle(install.state, title)}
+
+
+ Install {installTarget} to train {policyType}.
+
+
+
+
+
+ Training a {policyType} policy needs the{" "}
+ {packageName}{" "}
+ package (installed via{" "}
+ {installTarget}),
+ which isn't in this environment yet. Install it to train this policy.
+ >
+ }
+ doneDescription={ }
+ />
+
+
+
+ );
+};
+
+export default PolicyExtraDialog;
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..fee8d443f4308d9a0b1cc179dd64659b2277aa61
--- /dev/null
+++ b/src/components/training/config/AdvancedCard.tsx
@@ -0,0 +1,345 @@
+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
+
+ {/* An HF Jobs pod picks its own device, so this only applies locally. */}
+ {config.target.runner === 'local' && (
+
+
+ Device
+
+
+ updateConfig('policy_device', value === 'auto' ? undefined : value)
+ }
+ >
+
+
+
+
+ Auto
+ CPU
+
+
+
+ )}
+
+ 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..0103668e28f766ad25202767604e2f53a3571c4f
--- /dev/null
+++ b/src/components/training/config/EssentialsCard.tsx
@@ -0,0 +1,181 @@
+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
+ Multi-Task DiT
+ PI0
+ PI0 Fast
+ PI0.5
+ SmolVLA
+ GR00T N1.7
+ X-VLA
+ WALL-OSS
+ EO-1
+ Evo-1
+ MolmoAct2
+ VLA-JEPA
+ FastWAM
+ LingBot-VA
+ TD-MPC
+ VQ-BeT
+
+
+
+
+
+
+ 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/GrootCard.tsx b/src/components/training/config/GrootCard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ea820f0e65c77d44ee358817de3d5658a091ae63
--- /dev/null
+++ b/src/components/training/config/GrootCard.tsx
@@ -0,0 +1,178 @@
+import React, { useEffect, useRef } 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 { ConfigComponentProps } from '../types';
+
+// Sensible GR00T N1.7 defaults, seeded the first time the user picks the policy
+// so the form matches the canonical fine-tuning command out of the box.
+const GROOT_DEFAULTS = {
+ policy_base_model_path: 'nvidia/GR00T-N1.7-3B',
+ policy_embodiment_tag: 'new_embodiment',
+ policy_chunk_size: 16,
+ policy_n_action_steps: 16,
+ policy_use_relative_actions: true,
+ policy_relative_exclude_joints: ['gripper'],
+ policy_use_bf16: true,
+ dataset_image_transforms_enable: true,
+} as const;
+
+const GrootCard: React.FC = ({ config, updateConfig }) => {
+ const isGroot = config.policy_type === 'groot';
+ const seededDefaults = useRef(false);
+
+ // Seed defaults once when groot is selected. Each key is only written when
+ // still undefined, so the effect can't loop and never clobbers user edits.
+ useEffect(() => {
+ if (!isGroot || seededDefaults.current) return;
+ seededDefaults.current = true;
+ (Object.keys(GROOT_DEFAULTS) as (keyof typeof GROOT_DEFAULTS)[]).forEach((key) => {
+ if (config[key] === undefined) {
+ updateConfig(key, GROOT_DEFAULTS[key] as never);
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isGroot]);
+
+ if (!isGroot) return null;
+
+ const excludeJoints = (config.policy_relative_exclude_joints ?? []).join(', ');
+
+ return (
+
+
+ GR00T N1.7
+
+
+
+
+ Base Model Path
+
+
+ updateConfig('policy_base_model_path', e.target.value || undefined)
+ }
+ placeholder="nvidia/GR00T-N1.7-3B"
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+ HuggingFace repo of the pretrained GR00T backbone to fine-tune.
+
+
+
+
+
+
+ Embodiment Tag
+
+
+ updateConfig('policy_embodiment_tag', e.target.value || undefined)
+ }
+ placeholder="new_embodiment"
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+
+ Relative Exclude Joints
+
+
+ updateConfig(
+ 'policy_relative_exclude_joints',
+ e.target.value
+ .split(',')
+ .map((s) => s.trim())
+ .filter(Boolean),
+ )
+ }
+ placeholder="gripper"
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+ Comma-separated joints kept absolute when relative actions are on.
+
+
+
+
+
+ Chunk Size
+
+ updateConfig('policy_chunk_size', v)}
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+
+ Action Steps
+
+ updateConfig('policy_n_action_steps', v)}
+ className="bg-slate-900 border-slate-600 text-white rounded-lg"
+ />
+
+
+
+
+
+
+ updateConfig('policy_use_relative_actions', checked)
+ }
+ className="data-[state=checked]:bg-green-500"
+ />
+
+ Use Relative Actions
+
+
+
+
+ updateConfig('policy_use_bf16', checked)}
+ className="data-[state=checked]:bg-green-500"
+ />
+
+ Use bfloat16
+
+
+
+
+
+ updateConfig('dataset_image_transforms_enable', checked)
+ }
+ className="data-[state=checked]:bg-green-500"
+ />
+
+ Enable Image Augmentations
+
+
+
+
+
+ );
+};
+
+export default GrootCard;
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..cd4882ddd31fb91ce961881fb20d10396c613735
--- /dev/null
+++ b/src/components/training/types.ts
@@ -0,0 +1,83 @@
+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;
+
+ // GR00T-specific configuration (only used when policy_type === "groot").
+ dataset_image_transforms_enable?: boolean;
+ policy_base_model_path?: string;
+ policy_embodiment_tag?: string;
+ policy_chunk_size?: number;
+ policy_n_action_steps?: number;
+ policy_use_relative_actions?: boolean;
+ policy_relative_exclude_joints?: string[];
+ policy_use_bf16?: 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..80346858cf6b4530f636ffa426ffc8280e44ffd6
--- /dev/null
+++ b/src/contexts/ApiContext.tsx
@@ -0,0 +1,67 @@
+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");
+ const stored = window.localStorage.getItem(STORAGE_KEY);
+ for (const candidate of [fromQuery, stored]) {
+ if (!candidate) continue;
+ try {
+ const url = new URL(candidate);
+ if (url.protocol !== "http:" && url.protocol !== "https:") continue;
+ const clean = url.href.replace(/\/$/, "");
+ window.localStorage.setItem(STORAGE_KEY, clean);
+ return clean;
+ } catch {
+ console.warn("Invalid API URL, ignoring:", candidate);
+ }
+ }
+
+ return 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 && (
+