From 13caeb482b31cf3d23d53424af0410ce281ec540 Mon Sep 17 00:00:00 2001 From: Aiden Date: Tue, 19 May 2026 16:09:55 +1000 Subject: [PATCH] Initial commit --- .gitignore | 5 + README.md | 80 ++ index.html | 12 + package-lock.json | 1869 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 23 + public/favicon.svg | 4 + src/main.tsx | 1135 +++++++++++++++++++++++++++ src/styles.css | 521 ++++++++++++ src/vite-env.d.ts | 1 + tsconfig.json | 21 + vite.config.ts | 16 + 11 files changed, 3687 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 src/main.tsx create mode 100644 src/styles.css create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3d5b71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.env.* +npm-debug.log* diff --git a/README.md b/README.md new file mode 100644 index 0000000..a95cb66 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Unreal Outliner Control + +A Vite + React + TypeScript web app that mirrors the Unreal Editor Outliner for the currently open world. + +The app prefers Unreal Remote Control's WebSocket server, then falls back to HTTP when the socket is unavailable. + +By default it connects to: + +```text +ws://127.0.0.1:30020 +``` + +When WebSocket is available, the app sends Unreal's Remote Control `http` WebSocket message type and tunnels the same HTTP route: + +```http +PUT /remote/object/call +``` + +The HTTP fallback request goes through Vite's dev proxy and targets `http://127.0.0.1:30010/remote/object/call` by default. Both transports call: + +```json +{ + "objectPath": "/Script/UnrealEd.Default__EditorActorSubsystem", + "functionName": "GetAllLevelActors", + "parameters": {}, + "generateTransaction": false +} +``` + +Selecting an actor also loads a Details pane using: + +```http +PUT /remote/object/describe +PUT /remote/object/property +``` + +The property request uses `READ_ACCESS` and omits `propertyName`, which asks Unreal for all readable properties exposed on that UObject. + +## Run + +Start Unreal Editor, enable the Remote Control API, and make sure it is listening on `127.0.0.1:30010`. + +Then run: + +```bash +npm install +npm run dev +``` + +Open: + +```text +http://127.0.0.1:5173/ +``` + +If your Unreal Remote Control HTTP server is on another URL: + +```bash +set UNREAL_REMOTE_URL=http://127.0.0.1:30010 +npm run dev +``` + +If your Unreal Remote Control WebSocket server is on another URL: + +```bash +set VITE_UNREAL_WS_URL=ws://127.0.0.1:30020 +npm run dev +``` + +## Useful UI Libraries + +For an Unreal-like outliner, a custom tree/table gives the closest visual match because Unreal's Slate UI is not available as a web component. + +Good libraries if the app grows: + +- `react-arborist` for a performant tree with drag/drop and renaming. +- `@tanstack/react-table` plus `@tanstack/react-virtual` for a large outliner with resizable columns and virtualization. +- `ag-grid-community` if you want a full data-grid with tree data, sorting, filtering, and column tooling. + +This first version keeps the UI custom so it can look closer to the Unreal Outliner screenshot. diff --git a/index.html b/index.html new file mode 100644 index 0000000..67f5707 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + Unreal Outliner Control + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a80560f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1869 @@ +{ + "name": "unreal-outliner-control", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "unreal-outliner-control", + "version": "0.1.0", + "dependencies": { + "lucide-react": "^0.544.0", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.0.4", + "typescript": "^5.9.2", + "vite": "^7.1.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "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/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.357", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz", + "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/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/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..488b3fc --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "unreal-outliner-control", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173", + "build": "tsc && vite build", + "preview": "vite preview --host 127.0.0.1 --port 4173" + }, + "dependencies": { + "lucide-react": "^0.544.0", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.0.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "typescript": "^5.9.2", + "vite": "^7.1.7" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..ac789c1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..ea7002d --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,1135 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { + Box, + ChevronDown, + ChevronRight, + Cloud, + Eye, + Folder, + Gamepad2, + Layers, + Mountain, + PackagePlus, + RefreshCw, + Search, + Settings, + SlidersHorizontal, + Sun, + Triangle, + X, +} from "lucide-react"; +import "./styles.css"; + +type RemoteObject = Record; + +type ActorRow = { + id: string; + label: string; + type: string; + level: string; + idName: string; + folderPath: string; + objectPath: string; +}; + +type ActorReference = { + raw: unknown; + objectPath: string; +}; + +type TreeNode = { + id: string; + label: string; + type: string; + level: string; + idName: string; + objectPath?: string; + kind: "world" | "folder" | "actor"; + children: TreeNode[]; +}; + +type DetailProperty = { + name: string; + displayName: string; + category: string; + type: string; + value: unknown; + description: string; +}; + +type ObjectDetails = { + name: string; + className: string; + properties: DetailProperty[]; +}; + +const EDITOR_ACTOR_SUBSYSTEM = "/Script/UnrealEd.Default__EditorActorSubsystem"; +const EDITOR_LEVEL_LIBRARY = "/Script/UnrealEd.Default__EditorLevelLibrary"; +const UNREAL_WS_URL = import.meta.env.VITE_UNREAL_WS_URL ?? "ws://127.0.0.1:30020"; + +type RemoteTransport = "WebSocket" | "HTTP"; + +class RemoteCallError extends Error { + constructor( + message: string, + readonly isTransportError = false, + ) { + super(message); + } +} + +let activeTransport: RemoteTransport = "HTTP"; +let remoteSocket: WebSocket | null = null; +let remoteSocketPromise: Promise | null = null; +let nextRequestId = 1; +const disabledActorFunctions = new Set(); +const actorStringCallCache = new Map(); +const pendingSocketCalls = new Map< + number, + { + resolve: (payload: unknown) => void; + reject: (reason: Error) => void; + timeoutId: number; + } +>(); + +function asRecord(value: unknown): RemoteObject { + return value && typeof value === "object" && !Array.isArray(value) ? (value as RemoteObject) : {}; +} + +function isObjectRecord(value: unknown): value is RemoteObject { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function firstString(source: RemoteObject, keys: string[], fallback = ""): string { + for (const key of keys) { + const value = source[key]; + if (typeof value === "string" && value.trim()) { + return value; + } + if (typeof value === "number") { + return String(value); + } + } + return fallback; +} + +function findStringDeep(value: unknown, keys: string[], depth = 0): string { + if (depth > 3) { + return ""; + } + + if (typeof value === "string") { + return value; + } + + if (!isObjectRecord(value)) { + return ""; + } + + for (const key of keys) { + const direct = value[key]; + if (typeof direct === "string" && direct.trim()) { + return direct; + } + if (isObjectRecord(direct)) { + const nested = findStringDeep(direct, keys, depth + 1); + if (nested) { + return nested; + } + } + } + + for (const nestedValue of Object.values(value)) { + if (Array.isArray(nestedValue)) { + continue; + } + const nested = findStringDeep(nestedValue, keys, depth + 1); + if (nested) { + return nested; + } + } + + return ""; +} + +function extractObjectPath(value: unknown): string { + if (typeof value === "string") { + return value; + } + + return findStringDeep(value, [ + "objectPath", + "ObjectPath", + "path", + "Path", + "actorPath", + "ActorPath", + "softObjectPath", + "SoftObjectPath", + "name", + "Name", + ]); +} + +function nestedString(source: RemoteObject, keys: string[], fallback = ""): string { + for (const key of keys) { + const value = source[key]; + if (typeof value === "string" && value.trim()) { + return value; + } + const nested = asRecord(value); + const nestedValue = firstString(nested, ["Name", "name", "Path", "path", "ObjectPath", "objectPath"]); + if (nestedValue) { + return nestedValue; + } + } + return fallback; +} + +function basename(path: string): string { + const clean = path.replace(/["']/g, ""); + const colonName = clean.split(":").pop() ?? clean; + const dotName = colonName.split(".").pop() ?? colonName; + const slashName = dotName.split("/").pop() ?? dotName; + return slashName || clean || "Actor"; +} + +function cleanObjectName(name: string): string { + return name + .replace(/^Default__/, "") + .replace(/^UEDPIE_\d+_/, "") + .replace(/_\d+$/, (suffix) => suffix === "_0" ? "_0" : suffix); +} + +function deriveLevelNameFromPath(path: string): string { + if (!path) { + return ""; + } + + const clean = path.replace(/["']/g, ""); + const packagePath = clean.includes(":") ? clean.split(":")[0] : clean; + const objectName = packagePath.split(".").pop() ?? packagePath; + return cleanObjectName(basename(objectName)); +} + +function deriveType(actor: RemoteObject): string { + const direct = firstString(actor, ["Class", "class", "ClassName", "className", "Type", "type"]); + if (direct) { + return basename(direct).replace(/^BP_/, ""); + } + const path = firstString(actor, ["Path", "path", "ObjectPath", "objectPath"]); + const generatedClass = /\.(.+?)_C_\d+$/.exec(path)?.[1]; + return generatedClass ? basename(generatedClass) : "Actor"; +} + +function extractReturnValue(payload: unknown): unknown[] { + if (Array.isArray(payload)) { + return payload; + } + const record = asRecord(payload); + const returnValue = record.ReturnValue ?? record.returnValue ?? record.Result ?? record.result; + if (Array.isArray(returnValue)) { + return returnValue; + } + const nested = asRecord(returnValue); + for (const value of Object.values(nested)) { + if (Array.isArray(value)) { + return value; + } + } + return []; +} + +function normalizeActor(value: unknown, index: number): ActorRow { + const actor = asRecord(value); + const objectPath = extractObjectPath(value); + const pathName = cleanObjectName(basename(objectPath)); + const label = firstString(actor, ["ActorLabel", "actorLabel", "Label", "label", "DisplayName", "displayName"], pathName || `Actor ${index + 1}`); + const idName = firstString(actor, ["Name", "name", "IdName", "idName"], pathName || label); + const level = + deriveLevelNameFromPath(objectPath) || + nestedString(actor, ["Level", "level", "Outer", "outer", "Package", "package"], "Persistent Level"); + const folderPath = firstString(actor, ["FolderPath", "folderPath", "Folder", "folder", "ActorFolderPath", "actorFolderPath"]); + + return { + id: objectPath || `${label}-${index}`, + label, + type: deriveType(actor), + level: deriveLevelNameFromPath(level) || basename(level).replace(/^UEDPIE_\d+_/, "") || "Persistent Level", + idName, + folderPath, + objectPath, + }; +} + +function mergeActorDetails(actor: ActorRow, details: Partial): ActorRow { + return { + ...actor, + label: details.label && details.label !== "Actor" ? details.label : actor.label, + type: details.type && details.type !== "Actor" ? details.type : actor.type, + level: details.level && details.level !== "Persistent Level" ? details.level : actor.level, + idName: details.idName && details.idName !== "Actor" ? details.idName : actor.idName, + folderPath: details.folderPath || actor.folderPath, + }; +} + +function folderSegments(path: string): string[] { + return path + .split(/[\\/|]/) + .map((part) => part.trim()) + .filter((part) => part && part.toLowerCase() !== "none"); +} + +function getOrCreateFolder(parent: TreeNode, label: string, level: string): TreeNode { + const existing = parent.children.find((child) => child.kind === "folder" && child.label === label); + if (existing) { + return existing; + } + const folder: TreeNode = { + id: `${parent.id}/folder/${label}`, + label, + type: "Folder", + level, + idName: "", + kind: "folder", + children: [], + }; + parent.children.push(folder); + return folder; +} + +function buildTree(actors: ActorRow[]): TreeNode { + const rootLevelName = actors.find((actor) => actor.level && actor.level !== "Persistent Level")?.level; + const world: TreeNode = { + id: "world", + label: `${rootLevelName || "Open World"} (Editor)`, + type: "World", + level: "", + idName: "", + kind: "world", + children: [], + }; + + for (const actor of actors) { + let parent = world; + for (const segment of folderSegments(actor.folderPath)) { + parent = getOrCreateFolder(parent, segment, actor.level); + } + parent.children.push({ + ...actor, + kind: "actor", + children: [], + }); + } + + const sortNode = (node: TreeNode) => { + node.children.sort((a, b) => { + if (a.kind !== b.kind) { + return a.kind === "folder" ? -1 : 1; + } + return a.label.localeCompare(b.label); + }); + node.children.forEach(sortNode); + }; + sortNode(world); + return world; +} + +function visibleCount(node: TreeNode): number { + return node.children.reduce((total, child) => total + (child.kind === "actor" ? 1 : visibleCount(child)), 0); +} + +function findNodeById(node: TreeNode, id: string | null): TreeNode | null { + if (!id) { + return null; + } + if (node.id === id) { + return node; + } + + for (const child of node.children) { + const found = findNodeById(child, id); + if (found) { + return found; + } + } + + return null; +} + +async function parseRemoteResponse(response: Response): Promise { + const text = await response.text(); + if (!text.trim()) { + return null; + } + + try { + return JSON.parse(text); + } catch { + return text; + } +} + +function stringifyRemoteError(payload: unknown): string { + if (typeof payload === "string") { + return payload; + } + + const record = asRecord(payload); + const direct = firstString(record, ["errorMessage", "ErrorMessage", "message", "Message", "error", "Error"]); + if (direct) { + return direct; + } + + return JSON.stringify(payload, null, 2); +} + +function getActiveTransport(): RemoteTransport { + return activeTransport; +} + +function closeRemoteSocket(reason: Error) { + for (const call of pendingSocketCalls.values()) { + window.clearTimeout(call.timeoutId); + call.reject(reason); + } + pendingSocketCalls.clear(); + remoteSocket = null; + remoteSocketPromise = null; +} + +function parseSocketMessage(data: string | Blob): Promise { + if (typeof data === "string") { + return Promise.resolve(asRecord(JSON.parse(data))); + } + + return data.text().then((text) => asRecord(JSON.parse(text))); +} + +function connectRemoteSocket(): Promise { + if (remoteSocket?.readyState === WebSocket.OPEN) { + return Promise.resolve(remoteSocket); + } + + if (remoteSocketPromise) { + return remoteSocketPromise; + } + + remoteSocketPromise = new Promise((resolve, reject) => { + const socket = new WebSocket(UNREAL_WS_URL); + const timeoutId = window.setTimeout(() => { + socket.close(); + reject(new RemoteCallError(`Timed out connecting to ${UNREAL_WS_URL}`, true)); + remoteSocketPromise = null; + }, 1500); + + socket.onopen = () => { + window.clearTimeout(timeoutId); + remoteSocket = socket; + activeTransport = "WebSocket"; + resolve(socket); + }; + + socket.onerror = () => { + window.clearTimeout(timeoutId); + reject(new RemoteCallError(`Could not connect to ${UNREAL_WS_URL}`, true)); + remoteSocket = null; + remoteSocketPromise = null; + }; + + socket.onclose = () => { + closeRemoteSocket(new RemoteCallError("Unreal Remote Control WebSocket closed", true)); + }; + + socket.onmessage = (message) => { + parseSocketMessage(message.data) + .then((payload) => { + const requestId = Number(payload.RequestId ?? payload.Id ?? payload.id); + const pending = pendingSocketCalls.get(requestId); + if (!pending) { + return; + } + + pendingSocketCalls.delete(requestId); + window.clearTimeout(pending.timeoutId); + + const responseCode = Number(payload.ResponseCode ?? payload.responseCode ?? 200); + const responseBody = payload.ResponseBody ?? payload.responseBody ?? payload; + if (responseCode >= 200 && responseCode < 300) { + pending.resolve(responseBody); + } else { + pending.reject( + new RemoteCallError(`Remote Control returned ${responseCode}: ${stringifyRemoteError(responseBody)}`), + ); + } + }) + .catch((reason) => { + console.warn("Could not parse Unreal Remote Control WebSocket message", reason); + }); + }; + }); + + return remoteSocketPromise; +} + +async function callRemoteHttpRouteHttp(url: string, verb: "GET" | "PUT" | "POST" | "DELETE", body?: RemoteObject): Promise { + activeTransport = "HTTP"; + const response = await fetch(url, { + method: verb, + headers: body ? { "Content-Type": "application/json" } : undefined, + body: body ? JSON.stringify(body) : undefined, + }); + + const payload = await parseRemoteResponse(response); + + if (!response.ok) { + throw new RemoteCallError(`Remote Control returned ${response.status} ${response.statusText}: ${stringifyRemoteError(payload)}`); + } + + return payload; +} + +async function callRemoteHttpRouteWs(url: string, verb: "GET" | "PUT" | "POST" | "DELETE", body?: RemoteObject): Promise { + const socket = await connectRemoteSocket(); + const requestId = nextRequestId++; + + const response = new Promise((resolve, reject) => { + const timeoutId = window.setTimeout(() => { + pendingSocketCalls.delete(requestId); + reject(new RemoteCallError(`Timed out waiting for WebSocket response ${requestId}`, true)); + }, 8000); + + pendingSocketCalls.set(requestId, { + resolve, + reject, + timeoutId, + }); + }); + + try { + socket.send( + JSON.stringify({ + MessageName: "http", + Id: requestId, + Parameters: { + Url: url, + Verb: verb, + Body: body, + }, + }), + ); + } catch (reason) { + pendingSocketCalls.delete(requestId); + throw new RemoteCallError(reason instanceof Error ? reason.message : "Could not send WebSocket request", true); + } + + return response; +} + +async function callRemoteHttpRoute(url: string, verb: "GET" | "PUT" | "POST" | "DELETE", body?: RemoteObject): Promise { + try { + return await callRemoteHttpRouteWs(url, verb, body); + } catch (reason) { + if (reason instanceof RemoteCallError && reason.isTransportError) { + return callRemoteHttpRouteHttp(url, verb, body); + } + + throw reason; + } +} + +async function callRemoteFunction(objectPath: string, functionName: string, parameters: RemoteObject = {}): Promise { + return callRemoteHttpRoute("/remote/object/call", "PUT", { + objectPath, + functionName, + parameters, + generateTransaction: false, + }); +} + +async function describeRemoteObject(objectPath: string): Promise { + return callRemoteHttpRoute("/remote/object/describe", "PUT", { objectPath }); +} + +async function readRemoteObjectProperties(objectPath: string): Promise { + return callRemoteHttpRoute("/remote/object/property", "PUT", { + objectPath, + access: "READ_ACCESS", + }); +} + +function extractFirstReturnString(payload: unknown): string { + const record = asRecord(payload); + const returnValue = record.ReturnValue ?? record.returnValue ?? record.Result ?? record.result; + if (typeof returnValue === "string") { + return returnValue; + } + if (typeof returnValue === "number") { + return String(returnValue); + } + return firstString(asRecord(returnValue), ["Name", "name", "Label", "label", "DisplayName", "displayName"]); +} + +function getMetadataString(property: RemoteObject, key: string): string { + const metadata = asRecord(property.Metadata ?? property.metadata); + return firstString(metadata, [key, key.toLowerCase(), key.toUpperCase()]); +} + +function prettifyPropertyName(name: string): string { + return name + .replace(/^b([A-Z])/, "$1") + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/_/g, " ") + .trim(); +} + +function findPropertyValue(values: RemoteObject, name: string): unknown { + if (name in values) { + return values[name]; + } + + const lowerName = name.toLowerCase(); + const key = Object.keys(values).find((candidate) => candidate.toLowerCase() === lowerName); + return key ? values[key] : undefined; +} + +function extractPropertyValues(payload: unknown): RemoteObject { + const record = asRecord(payload); + const candidates = [ + record, + asRecord(record.ResponseBody), + asRecord(record.responseBody), + asRecord(record.Properties), + asRecord(record.properties), + ]; + + for (const candidate of candidates) { + const keys = Object.keys(candidate); + if (keys.length > 0 && !keys.includes("ResponseCode") && !keys.includes("RequestId")) { + return candidate; + } + } + + return {}; +} + +function normalizeDetails(describePayload: unknown, propertyPayload: unknown): ObjectDetails { + const description = asRecord(describePayload); + const values = extractPropertyValues(propertyPayload); + const properties = Array.isArray(description.Properties) + ? description.Properties + : Array.isArray(description.properties) + ? description.properties + : []; + + return { + name: firstString(description, ["Name", "name"], "Object"), + className: firstString(description, ["Class", "class"], "UObject"), + properties: properties.map((rawProperty): DetailProperty => { + const property = asRecord(rawProperty); + const name = firstString(property, ["Name", "name"], "Property"); + return { + name, + displayName: getMetadataString(property, "DisplayName") || prettifyPropertyName(name), + category: getMetadataString(property, "Category") || "General", + type: firstString(property, ["Type", "type"], "Property"), + value: findPropertyValue(values, name), + description: firstString(property, ["Description", "description"]), + }; + }), + }; +} + +function formatDetailValue(value: unknown): string { + if (value === undefined) { + return "Unavailable"; + } + if (value === null) { + return "None"; + } + if (typeof value === "boolean") { + return value ? "True" : "False"; + } + if (typeof value === "number") { + return Number.isInteger(value) ? String(value) : value.toFixed(3).replace(/\.?0+$/, ""); + } + if (typeof value === "string") { + return value || "None"; + } + if (Array.isArray(value)) { + return `${value.length} item${value.length === 1 ? "" : "s"}`; + } + + const record = asRecord(value); + const vectorKeys = ["X", "Y", "Z"].filter((key) => key in record); + if (vectorKeys.length === 3) { + return vectorKeys.map((key) => `${key}: ${formatDetailValue(record[key])}`).join(" "); + } + + const rotatorKeys = ["Roll", "Pitch", "Yaw"].filter((key) => key in record); + if (rotatorKeys.length === 3) { + return rotatorKeys.map((key) => `${key}: ${formatDetailValue(record[key])}`).join(" "); + } + + const objectPath = extractObjectPath(value); + if (objectPath) { + return cleanObjectName(basename(objectPath)); + } + + return JSON.stringify(value); +} + +async function getAllLevelActors(): Promise { + const attempts = [ + { objectPath: EDITOR_ACTOR_SUBSYSTEM, functionName: "GetAllLevelActors" }, + { objectPath: EDITOR_LEVEL_LIBRARY, functionName: "GetAllLevelActors" }, + ]; + + const errors: string[] = []; + for (const attempt of attempts) { + try { + return await callRemoteFunction(attempt.objectPath, attempt.functionName); + } catch (reason) { + errors.push(`${attempt.objectPath}.${attempt.functionName}: ${reason instanceof Error ? reason.message : String(reason)}`); + } + } + + throw new Error(errors.join("\n\n")); +} + +async function tryRemoteString(objectPath: string, functionName: string): Promise { + if (disabledActorFunctions.has(functionName)) { + return ""; + } + + const cacheKey = `${objectPath}|${functionName}`; + if (actorStringCallCache.has(cacheKey)) { + return actorStringCallCache.get(cacheKey) ?? ""; + } + + try { + const value = extractFirstReturnString(await callRemoteFunction(objectPath, functionName)); + actorStringCallCache.set(cacheKey, value); + return value; + } catch (reason) { + if (reason instanceof RemoteCallError && !reason.isTransportError) { + disabledActorFunctions.add(functionName); + } + actorStringCallCache.set(cacheKey, ""); + return ""; + } +} + +async function tryRemoteObjectPath(objectPath: string, functionName: string): Promise { + try { + const payload = await callRemoteFunction(objectPath, functionName); + const record = asRecord(payload); + return extractObjectPath(record.ReturnValue ?? record.returnValue ?? record.Result ?? record.result); + } catch { + return ""; + } +} + +async function enrichActor(actor: ActorRow): Promise { + if (!actor.objectPath) { + return actor; + } + + const [label, folderPath] = await Promise.all([ + tryRemoteString(actor.objectPath, "GetActorLabel"), + tryRemoteString(actor.objectPath, "GetFolderPath"), + ]); + + const typeFromPath = actor.objectPath.includes(".") + ? cleanObjectName(basename(actor.objectPath).replace(/_\d+$/, "")) + : ""; + + return mergeActorDetails(actor, { + label: label || actor.label, + folderPath: folderPath || actor.folderPath, + level: deriveLevelNameFromPath(actor.objectPath) || actor.level, + type: actor.type === "Actor" && typeFromPath ? typeFromPath : actor.type, + }); +} + +async function enrichActors(actors: ActorRow[]): Promise { + const sampleActor = actors.find((actor) => actor.objectPath); + if (sampleActor) { + await Promise.all([ + tryRemoteString(sampleActor.objectPath, "GetActorLabel"), + tryRemoteString(sampleActor.objectPath, "GetFolderPath"), + ]); + } + + const enriched: ActorRow[] = []; + for (let index = 0; index < actors.length; index += 4) { + enriched.push(...(await Promise.all(actors.slice(index, index + 4).map(enrichActor)))); + } + return enriched; +} + +function actorIcon(type: string) { + const lower = type.toLowerCase(); + if (lower.includes("directional") || lower.includes("light")) return Sun; + if (lower.includes("fog")) return Cloud; + if (lower.includes("sky") || lower.includes("atmosphere")) return Mountain; + if (lower.includes("start") || lower.includes("pawn")) return Gamepad2; + if (lower.includes("mesh")) return Box; + return Triangle; +} + +function NodeIcon({ node }: { node: TreeNode }) { + if (node.kind === "folder") { + return ; + } + if (node.kind === "world") { + return ; + } + const Icon = actorIcon(node.type); + return ; +} + +function OutlinerRow({ + node, + depth, + expanded, + selectedId, + query, + onToggle, + onSelect, +}: { + node: TreeNode; + depth: number; + expanded: Set; + selectedId: string | null; + query: string; + onToggle: (node: TreeNode) => void; + onSelect: (node: TreeNode) => void; +}) { + const hasChildren = node.children.length > 0; + const isOpen = expanded.has(node.id); + const matches = + !query || + [node.label, node.type, node.level, node.idName].some((field) => field.toLowerCase().includes(query.toLowerCase())); + + const visibleChildren = node.children.filter((child) => nodeMatchesQuery(child, query)); + + if (!matches && visibleChildren.length === 0) { + return null; + } + + return ( + <> + + {(isOpen || query) && + visibleChildren.map((child) => ( + + ))} + + ); +} + +function nodeMatchesQuery(node: TreeNode, query: string): boolean { + if (!query) return true; + const lower = query.toLowerCase(); + const selfMatches = [node.label, node.type, node.level, node.idName].some((field) => field.toLowerCase().includes(lower)); + return selfMatches || node.children.some((child) => nodeMatchesQuery(child, query)); +} + +function DetailsPanel({ + node, + details, + isLoading, + error, +}: { + node: TreeNode | null; + details: ObjectDetails | null; + isLoading: boolean; + error: string | null; +}) { + const [query, setQuery] = React.useState(""); + + const groupedProperties = React.useMemo(() => { + const lowerQuery = query.toLowerCase(); + const visibleProperties = (details?.properties ?? []).filter((property) => { + if (!lowerQuery) return true; + return [property.displayName, property.name, property.type, property.category, formatDetailValue(property.value)].some((field) => + field.toLowerCase().includes(lowerQuery), + ); + }); + + return visibleProperties.reduce>((groups, property) => { + const category = property.category || "General"; + groups[category] = groups[category] ?? []; + groups[category].push(property); + return groups; + }, {}); + }, [details, query]); + + return ( + + ); +} + +function App() { + const [actors, setActors] = React.useState([]); + const [query, setQuery] = React.useState(""); + const [expanded, setExpanded] = React.useState>(new Set(["world"])); + const [selectedId, setSelectedId] = React.useState(null); + const [status, setStatus] = React.useState("Ready"); + const [transport, setTransport] = React.useState("HTTP"); + const [isLoading, setIsLoading] = React.useState(false); + const [error, setError] = React.useState(null); + const [details, setDetails] = React.useState(null); + const [detailsError, setDetailsError] = React.useState(null); + const [isDetailsLoading, setIsDetailsLoading] = React.useState(false); + + const tree = React.useMemo(() => buildTree(actors), [actors]); + const selectedNode = React.useMemo(() => findNodeById(tree, selectedId), [tree, selectedId]); + + const refresh = React.useCallback(async () => { + setIsLoading(true); + setError(null); + setStatus("Loading actors..."); + try { + const payload = await getAllLevelActors(); + const actorRefs: ActorReference[] = extractReturnValue(payload).map((raw) => ({ raw, objectPath: extractObjectPath(raw) })); + const rows = await enrichActors(actorRefs.map(({ raw }, index) => normalizeActor(raw, index))); + setTransport(getActiveTransport()); + setActors(rows); + setExpanded((current) => new Set([...current, "world"])); + setStatus(`Synced ${rows.length} actor${rows.length === 1 ? "" : "s"}`); + } catch (reason) { + const message = reason instanceof Error ? reason.message : "Could not reach Unreal Remote Control"; + setError(message); + setStatus("Connection failed"); + } finally { + setIsLoading(false); + } + }, []); + + React.useEffect(() => { + refresh(); + }, [refresh]); + + React.useEffect(() => { + let isCancelled = false; + + if (!selectedNode?.objectPath) { + setDetails(null); + setDetailsError(null); + setIsDetailsLoading(false); + return; + } + + setIsDetailsLoading(true); + setDetailsError(null); + + Promise.all([describeRemoteObject(selectedNode.objectPath), readRemoteObjectProperties(selectedNode.objectPath)]) + .then(([description, properties]) => { + if (!isCancelled) { + setDetails(normalizeDetails(description, properties)); + setTransport(getActiveTransport()); + } + }) + .catch((reason) => { + if (!isCancelled) { + setDetails(null); + setDetailsError(reason instanceof Error ? reason.message : "Could not load object properties"); + } + }) + .finally(() => { + if (!isCancelled) { + setIsDetailsLoading(false); + } + }); + + return () => { + isCancelled = true; + }; + }, [selectedNode?.objectPath]); + + const toggleNode = (node: TreeNode) => { + setExpanded((current) => { + const next = new Set(current); + if (next.has(node.id)) { + next.delete(node.id); + } else { + next.add(node.id); + } + return next; + }); + }; + + return ( +
+
+
+
+ + Outliner + +
+
+ + Levels +
+
+ + Layers +
+
+ +
+ + + + + + +
+ +
+ + Item Label + Type + Level + ID Name +
+ +
+
+ {error ? ( +
+ {status} + {error} + Remote Control is reachable, but Unreal rejected the object call. Check the exact error above. +
+ ) : ( + setSelectedId(node.id)} + /> + )} +
+ + +
+ +
+ {query ? `${visibleCount(tree)} actors total` : `${actors.length} actor${actors.length === 1 ? "" : "s"}`} + {status} ยท {transport} +
+
+
+ ); +} + +createRoot(document.getElementById("root")!).render(); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..ec0e92e --- /dev/null +++ b/src/styles.css @@ -0,0 +1,521 @@ +:root { + color-scheme: dark; + font-family: "Segoe UI", "Inter", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + background: #101010; + color: #c7c7c7; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; + background: #101010; +} + +button, +input { + font: inherit; +} + +.shell { + min-height: 100vh; + padding: 10px; + background: #101010; +} + +.panel { + width: min(1220px, 100%); + min-height: calc(100vh - 20px); + height: calc(100vh - 20px); + display: grid; + grid-template-rows: 48px 56px 36px 1fr 50px; + border: 1px solid #0a0a0a; + background: #171717; + box-shadow: inset 0 0 0 1px #232323; +} + +.tabs { + display: flex; + align-items: stretch; + height: 48px; + background: #1f1f1f; + border-bottom: 1px solid #0c0c0c; +} + +.tab { + display: flex; + align-items: center; + gap: 10px; + min-width: 240px; + padding: 0 14px; + color: #bdbdbd; + font-size: 20px; + border-right: 1px solid #111; +} + +.tab.active { + color: #f0f0f0; + background: #242424; + box-shadow: inset 0 3px 0 #2d6eaf; +} + +.tab svg:last-child { + margin-left: auto; +} + +.toolbar { + display: grid; + grid-template-columns: 36px 30px minmax(160px, 1fr) 36px 36px 36px; + align-items: center; + gap: 8px; + padding: 7px 12px; + background: #262626; +} + +.icon-button { + width: 32px; + height: 32px; + display: inline-grid; + place-items: center; + border: 0; + padding: 0; + color: #cfcfcf; + background: transparent; + border-radius: 3px; + cursor: pointer; +} + +.icon-button:hover:not(:disabled) { + background: #363636; +} + +.icon-button:disabled { + color: #777; + cursor: wait; +} + +.search { + height: 36px; + min-width: 0; + display: grid; + grid-template-columns: 30px 1fr 24px; + align-items: center; + padding: 0 8px; + color: #cfcfcf; + background: #0d0d0d; + border: 1px solid #343434; + border-radius: 18px; + box-shadow: inset 0 0 0 1px #050505; +} + +.search::after { + content: ""; + width: 10px; + height: 10px; + border-right: 3px solid #cfcfcf; + border-bottom: 3px solid #cfcfcf; + transform: rotate(45deg) translateY(-3px); + justify-self: center; +} + +.search input { + width: 100%; + min-width: 0; + color: #dadada; + background: transparent; + border: 0; + outline: 0; + font-size: 20px; +} + +.search input::placeholder { + color: #666; +} + +.columns { + display: grid; + grid-template-columns: 38px minmax(260px, 1.3fr) minmax(140px, 0.55fr) minmax(130px, 0.52fr) minmax(150px, 0.62fr); + align-items: center; + background: #303030; + color: #c7c7c7; + font-size: 20px; +} + +.columns span { + height: 100%; + display: flex; + align-items: center; + padding: 0 8px; + border-right: 2px solid #1b1b1b; +} + +.columns .visibility { + justify-content: center; + padding: 0; +} + +.main-content { + min-height: 0; + overflow: hidden; + display: grid; + grid-template-rows: minmax(180px, 1fr) minmax(250px, 0.9fr); + background: #171717; +} + +.rows { + min-height: 0; + overflow: auto; + background: repeating-linear-gradient(#181818 0 30px, #141414 30px 60px); +} + +.outliner-row { + width: 100%; + min-width: 760px; + height: 30px; + display: grid; + grid-template-columns: minmax(298px, 1.3fr) minmax(140px, 0.55fr) minmax(130px, 0.52fr) minmax(150px, 0.62fr); + align-items: center; + color: #bebebe; + background: transparent; + border: 0; + padding: 0; + text-align: left; + cursor: default; +} + +.outliner-row.selected { + background: #2b4056; + color: #f0f0f0; +} + +.outliner-row:hover:not(.selected) { + background: rgba(92, 105, 116, 0.24); + color: #d9d9d9; +} + +.cell { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 0 12px; + font-size: 20px; + color: inherit; +} + +.item-cell { + display: flex; + align-items: center; + gap: 5px; + padding-left: 42px; +} + +.indent { + width: calc(var(--depth) * 18px); + flex: 0 0 auto; +} + +.expander { + width: 18px; + height: 22px; + display: inline-grid; + place-items: center; + flex: 0 0 auto; + color: #adadad; +} + +.label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.type-cell, +.level-cell, +.id-cell { + color: #8f8f8f; +} + +.folder-icon { + color: #d0a64f; + fill: #d0a64f; +} + +.world-icon, +.actor-icon { + color: #c2c2c2; +} + +.footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 0 20px; + color: #cfcfcf; + background: #303030; + font-size: 20px; +} + +.empty { + min-height: 220px; + display: grid; + align-content: center; + justify-items: center; + gap: 10px; + padding: 24px; + color: #bdbdbd; + text-align: center; + font-size: 16px; +} + +.empty strong { + color: #f0f0f0; + font-size: 22px; +} + +.details-panel { + min-height: 0; + display: grid; + grid-template-rows: 38px 58px 44px 1fr; + border-top: 2px solid #0d0d0d; + background: #202020; + color: #cfcfcf; +} + +.details-tabs { + display: flex; + align-items: stretch; + background: #151515; +} + +.details-tab { + display: flex; + align-items: center; + gap: 9px; + min-width: 210px; + padding: 0 12px; + color: #c7c7c7; + font-size: 20px; + background: #232323; + border-right: 1px solid #101010; +} + +.details-tab.muted { + color: #a8a8a8; + background: #181818; +} + +.details-tab.active { + color: #dedede; +} + +.details-tab svg:last-child { + margin-left: auto; +} + +.details-object { + display: grid; + grid-template-columns: 28px 1fr; + align-items: center; + gap: 10px; + padding: 9px 14px; + border-bottom: 1px solid #171717; + background: #242424; +} + +.details-object strong, +.details-object span { + display: block; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.details-object strong { + color: #f1f1f1; + font-size: 18px; + font-weight: 500; +} + +.details-object span { + margin-top: 2px; + color: #a8a8a8; + font-size: 14px; +} + +.details-search-row { + display: grid; + grid-template-columns: minmax(140px, 1fr) 34px; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-bottom: 1px solid #141414; + background: #262626; +} + +.details-search { + height: 31px; + display: grid; + grid-template-columns: 26px 1fr; + align-items: center; + padding: 0 8px; + color: #cfcfcf; + background: #0d0d0d; + border: 1px solid #343434; + border-radius: 15px; +} + +.details-search input { + min-width: 0; + width: 100%; + color: #d8d8d8; + background: transparent; + border: 0; + outline: 0; + font-size: 17px; +} + +.details-tool { + width: 30px; + height: 30px; + display: grid; + place-items: center; + border: 0; + border-radius: 3px; + color: #c7c7c7; + background: transparent; +} + +.details-tool:hover { + background: #363636; +} + +.details-categories { + min-height: 0; + overflow: auto; + background: #1f1f1f; +} + +.detail-category h3 { + height: 32px; + display: flex; + align-items: center; + gap: 6px; + margin: 0; + padding: 0 10px; + color: #d0d0d0; + background: #303030; + border-top: 1px solid #393939; + border-bottom: 1px solid #171717; + font-size: 16px; + font-weight: 600; +} + +.detail-row { + min-height: 40px; + display: grid; + grid-template-columns: minmax(180px, 0.45fr) minmax(220px, 0.55fr); + border-bottom: 1px solid #171717; +} + +.detail-name, +.detail-value { + min-width: 0; + display: flex; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0 14px; + font-size: 16px; +} + +.detail-name { + color: #c8c8c8; + border-right: 1px solid #151515; +} + +.detail-value { + color: #e0e0e0; + background: #242424; +} + +.details-empty { + min-height: 130px; + display: grid; + place-items: center; + padding: 20px; + color: #a8a8a8; + text-align: center; + font-size: 16px; +} + +.spin { + animation: spin 0.85s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 720px) { + .shell { + padding: 0; + } + + .panel { + min-height: 100vh; + height: 100vh; + border: 0; + grid-template-rows: 44px 52px 34px 1fr 44px; + } + + .main-content { + grid-template-rows: minmax(170px, 1fr) minmax(260px, 0.95fr); + } + + .tab { + min-width: auto; + flex: 1; + font-size: 16px; + padding: 0 8px; + } + + .tab svg:last-child, + .tab:not(.active) span { + display: none; + } + + .toolbar { + grid-template-columns: 32px 28px minmax(120px, 1fr) 32px 32px 32px; + gap: 4px; + padding: 6px; + } + + .columns, + .outliner-row { + min-width: 720px; + } + + .details-tab { + min-width: 160px; + font-size: 16px; + } + + .detail-row { + grid-template-columns: minmax(130px, 0.42fr) minmax(170px, 0.58fr); + } + + .footer { + font-size: 15px; + padding: 0 12px; + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..596336d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b9418de --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +const unrealRemoteUrl = process.env.UNREAL_REMOTE_URL ?? "http://127.0.0.1:30010"; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + "/remote": { + target: unrealRemoteUrl, + changeOrigin: true, + }, + }, + }, +});