feat(interface): frontend updates and Nexus branding

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-11 07:25:14 -05:00
parent 53eaed4c7f
commit 5c2df30342
25 changed files with 1139 additions and 329 deletions
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Generated Regular → Executable
+217 -204
View File
@@ -1,12 +1,12 @@
{
"name": "web",
"version": "0.0.0",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "web",
"version": "0.0.0",
"version": "1.0.0",
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
@@ -24,13 +24,13 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/helper-validator-identifier": "^7.29.7",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -39,9 +39,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -49,21 +49,21 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"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",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-compilation-targets": "^7.29.7",
"@babel/helper-module-transforms": "^7.29.7",
"@babel/helpers": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -80,14 +80,14 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -97,14 +97,14 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -114,9 +114,9 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -124,29 +124,29 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7"
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.6"
"@babel/helper-module-imports": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7",
"@babel/traverse": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@@ -156,9 +156,9 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -166,9 +166,9 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -176,9 +176,9 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -186,27 +186,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0"
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -216,33 +216,33 @@
}
},
"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==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/code-frame": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
"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",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-globals": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7",
"debug": "^4.3.1"
},
"engines": {
@@ -250,23 +250,23 @@
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -276,9 +276,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -557,14 +557,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
"@tybys/wasm-util": "^0.10.2"
},
"funding": {
"type": "github",
@@ -576,9 +576,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.124.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -586,9 +586,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"cpu": [
"arm64"
],
@@ -603,9 +603,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"cpu": [
"arm64"
],
@@ -620,9 +620,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"cpu": [
"x64"
],
@@ -637,9 +637,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"cpu": [
"x64"
],
@@ -654,9 +654,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"cpu": [
"arm"
],
@@ -671,9 +671,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"cpu": [
"arm64"
],
@@ -688,9 +688,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"cpu": [
"arm64"
],
@@ -705,9 +705,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"cpu": [
"ppc64"
],
@@ -722,9 +722,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"cpu": [
"s390x"
],
@@ -739,9 +739,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"cpu": [
"x64"
],
@@ -756,9 +756,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"cpu": [
"x64"
],
@@ -773,9 +773,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"cpu": [
"arm64"
],
@@ -790,9 +790,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"cpu": [
"wasm32"
],
@@ -800,18 +800,18 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.9.2",
"@emnapi/runtime": "1.9.2",
"@napi-rs/wasm-runtime": "^1.1.3"
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
},
"engines": {
"node": ">=14.0.0"
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"cpu": [
"arm64"
],
@@ -826,9 +826,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -850,9 +850,9 @@
"license": "MIT"
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -991,9 +991,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
"integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
"version": "2.10.38",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
"integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -1059,9 +1059,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001788",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
"integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
"version": "1.0.30001799",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
"dev": true,
"funding": [
{
@@ -1188,9 +1188,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.336",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz",
"integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==",
"version": "1.5.375",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
"integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
"dev": true,
"license": "ISC"
},
@@ -1644,10 +1644,20 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -2042,9 +2052,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"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": [
{
@@ -2068,11 +2078,14 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.37",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
"version": "2.0.48",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
"integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/optionator": {
"version": "0.9.4",
@@ -2178,9 +2191,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -2198,7 +2211,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -2258,14 +2271,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.124.0",
"@rolldown/pluginutils": "1.0.0-rc.15"
"@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -2274,27 +2287,27 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
"@rolldown/binding-android-arm64": "1.0.3",
"@rolldown/binding-darwin-arm64": "1.0.3",
"@rolldown/binding-darwin-x64": "1.0.3",
"@rolldown/binding-freebsd-x64": "1.0.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
"@rolldown/binding-linux-arm64-musl": "1.0.3",
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
"@rolldown/binding-linux-x64-gnu": "1.0.3",
"@rolldown/binding-linux-x64-musl": "1.0.3",
"@rolldown/binding-openharmony-arm64": "1.0.3",
"@rolldown/binding-wasm32-wasi": "1.0.3",
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
"@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.15",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
@@ -2374,9 +2387,9 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2453,17 +2466,17 @@
}
},
"node_modules/vite": {
"version": "8.0.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.8",
"rolldown": "1.0.0-rc.15",
"tinyglobby": "^0.2.15"
"postcss": "^8.5.15",
"rolldown": "1.0.3",
"tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -2479,7 +2492,7 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0",
"@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
Regular → Executable
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "web",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 185 KiB

Regular → Executable
View File
Regular → Executable
+76 -12
View File
@@ -19,10 +19,12 @@ function timeAgo(epochSeconds) {
function App() {
const [currentPage, setCurrentPage] = useState("chatbot");
const [status, setStatus] = useState("Checking Synapse...");
const [version, setVersion] = useState("");
const [ollamaStatus, setOllamaStatus] = useState("checking");
const [showStatusTooltip, setShowStatusTooltip] = useState(false);
const [activeConversationId, setActiveConversationId] = useState(() => crypto.randomUUID());
const [isModelPulling, setIsModelPulling] = useState(false);
const [conversations, setConversations] = useState([]);
const [search, setSearch] = useState("");
const [hoveredConvId, setHoveredConvId] = useState(null);
@@ -33,6 +35,7 @@ function App() {
.then(r => r.json())
.then(d => {
setStatus(d.status ?? "Unknown");
setVersion(d.version ?? "");
setOllamaStatus(d.ollama ?? "unavailable");
})
.catch(() => {
@@ -91,6 +94,25 @@ function App() {
setCurrentPage("chatbot");
};
const renameConversation = async (e, conv) => {
e.stopPropagation();
const current = conv.title || conv.preview || "";
const next = window.prompt("Rename conversation:", current);
if (next == null) return;
const title = next.trim();
if (!title || title === current) return;
try {
const response = await fetch(`${API_BASE}/conversations/${conv.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title }),
});
if (response.ok) loadConversations(search);
} catch (error) {
console.error("Failed to rename conversation:", error);
}
};
const deleteConversation = async (e, conversationId) => {
e.stopPropagation();
if (!window.confirm("Delete this conversation?")) return;
@@ -108,9 +130,9 @@ function App() {
};
const navItems = [
{ key: "chatbot", label: "💬 Chatbot" },
{ key: "playbook", label: "📖 Playbook" },
{ key: "models", label: "🤖 Models" },
{ key: "chatbot", label: "💬 Chat" },
{ key: "playbook", label: "📖 Playbooks" },
{ key: "models", label: "🤖 Models", badge: isModelPulling },
{ key: "memory", label: "🧠 Memory" },
{ key: "settings", label: "⚙️ Settings" },
];
@@ -120,6 +142,7 @@ function App() {
{/* Sidebar */}
<aside style={{
width: "260px",
flexShrink: 0,
background: "#0a0a0a",
borderRight: "1px solid #333",
display: "flex",
@@ -138,6 +161,9 @@ function App() {
}}>
<img src="/n small.png" alt="Logo" style={{ width: "48px", height: "48px", objectFit: "contain", borderRadius: "8px" }} />
<h1 style={{ fontSize: "1rem", margin: 0, color: "#007acc", textAlign: "center" }}>NexusOS</h1>
{version && (
<span style={{ fontSize: "0.7rem", color: "#666", marginTop: "-0.5rem", letterSpacing: "0.02em" }}>v{version}</span>
)}
{/* Status Indicator */}
<div style={{ position: "relative" }}>
@@ -197,9 +223,22 @@ function App() {
fontSize: "0.85rem",
textAlign: "left",
transition: "all 0.15s",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
{item.label}
{item.badge && (
<span style={{
width: "8px",
height: "8px",
borderRadius: "50%",
background: "#28a745",
flexShrink: 0,
animation: "pulse 1.2s ease-in-out infinite",
}} />
)}
</button>
))}
</nav>
@@ -284,13 +323,30 @@ function App() {
overflow: "hidden",
textOverflow: "ellipsis",
}}>
{conv.preview || "Conversation"}
{conv.title || conv.preview || "Conversation"}
</div>
<div style={{ fontSize: "0.7rem", color: "#666", marginTop: "0.1rem" }}>
{timeAgo(conv.timestamp)}
</div>
</div>
{isHover && (
<>
<button
onClick={(e) => renameConversation(e, conv)}
title="Rename"
style={{
padding: "0.15rem 0.4rem",
background: "transparent",
color: "#888",
border: "1px solid #333",
borderRadius: "4px",
cursor: "pointer",
fontSize: "0.7rem",
flexShrink: 0,
}}
>
</button>
<button
onClick={(e) => deleteConversation(e, conv.id)}
title="Delete"
@@ -307,6 +363,7 @@ function App() {
>
</button>
</>
)}
</div>
);
@@ -317,19 +374,26 @@ function App() {
</aside>
{/* Main Content */}
<main style={{ flexGrow: 1, padding: "1.5rem", display: "flex", flexDirection: "column", overflowY: "auto", minHeight: 0 }}>
{currentPage === "chatbot" ? (
<main style={{ flexGrow: 1, padding: "1.5rem", display: "flex", flexDirection: "column", overflowY: "auto", minHeight: 0, minWidth: 0 }}>
{/* Models stays mounted so active downloads survive page navigation */}
<div style={{
display: currentPage === "models" ? "flex" : "none",
flexDirection: "column",
flexGrow: 1,
minHeight: 0,
}}>
<Models onPullStateChange={setIsModelPulling} />
</div>
{currentPage === "chatbot" && (
<Chatbot
status={status}
conversationId={activeConversationId}
setConversationId={setActiveConversationId}
onConversationChanged={() => loadConversations(search)}
/>
)
: currentPage === "playbook" ? <Playbook status={status} />
: currentPage === "models" ? <Models status={status} />
: currentPage === "memory" ? <Memory />
: <Settings />}
)}
{currentPage === "playbook" && <Playbook />}
{currentPage === "memory" && <Memory />}
{currentPage === "settings" && <Settings />}
</main>
</div>
);
Regular → Executable
+33 -5
View File
@@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from "react";
import { API_BASE } from "./config";
import { Markdown } from "./Markdown";
export function Chatbot({ status, conversationId, setConversationId, onConversationChanged }) {
export function Chatbot({ conversationId, setConversationId, onConversationChanged }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
@@ -67,7 +67,7 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model }),
});
} catch {}
} catch { /* persisting the model choice is best-effort */ }
};
const startNewChat = () => {
@@ -145,7 +145,18 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
if (!payload.trim()) { pendingEventType = null; continue; }
if (pendingEventType === "meta") {
try { setLastStats(JSON.parse(payload)); } catch { /* ignore */ }
try {
const stats = JSON.parse(payload);
setLastStats(stats);
// Tag this message with the model that answered
if (stats.model) {
setMessages(prev => {
const updated = [...prev];
updated[assistantIndex] = { ...updated[assistantIndex], model: stats.model };
return updated;
});
}
} catch { /* ignore */ }
pendingEventType = null;
continue;
}
@@ -158,6 +169,18 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
pendingEventType = null;
continue;
}
if (pendingEventType === "done") {
// Answer is complete; re-enable input while the backend finishes
// slow post-processing (title, memory) on the still-open stream.
setLoading(false);
pendingEventType = null;
continue;
}
if (pendingEventType === "title") {
if (onConversationChanged) onConversationChanged();
pendingEventType = null;
continue;
}
if (pendingEventType === "error") {
try {
const err = JSON.parse(payload);
@@ -171,11 +194,14 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
}
pendingEventType = null;
let token = payload;
try { token = JSON.parse(payload); } catch { /* plain text fallback */ }
setMessages(prev => {
const updated = [...prev];
updated[assistantIndex] = {
...updated[assistantIndex],
content: (updated[assistantIndex].content || "") + payload,
content: (updated[assistantIndex].content || "") + token,
};
return updated;
});
@@ -368,12 +394,14 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
>
<div
style={{
maxWidth: "70%",
maxWidth: msg.role === "user" ? "70%" : "90%",
width: msg.role === "assistant" ? "100%" : undefined,
padding: "0.9rem",
borderRadius: "10px",
background: msg.role === "user" ? "#007acc" : "#262626",
color: "#eee",
wordWrap: "break-word",
boxSizing: "border-box",
}}
>
{msg.role === "user"
Regular → Executable
+103 -38
View File
@@ -1,43 +1,118 @@
import { useState } from "react";
// Parse content into an array of {type, value, lang, streaming} blocks.
// Handles:
// - ```lang\ncode``` (well-formed)
// - ```code``` (no language, no newline — model formatting bug)
// - ```lang\ncode (unclosed — streaming in progress)
function parseBlocks(content) {
const blocks = [];
let i = 0;
while (i < content.length) {
const fenceStart = content.indexOf("```", i);
if (fenceStart === -1) {
if (i < content.length) blocks.push({ type: "text", value: content.slice(i) });
break;
}
if (fenceStart > i) blocks.push({ type: "text", value: content.slice(i, fenceStart) });
let j = fenceStart + 3;
let lang = "";
// Language specifier is valid only when word-chars are followed by a newline.
// If there's no newline (e.g. ```pythonprint(...)) treat everything as code.
const langMatch = content.slice(j).match(/^(\w+)(\r?\n)/);
if (langMatch) {
lang = langMatch[1];
j += langMatch[0].length;
} else if (/^\r?\n/.test(content.slice(j))) {
j += content[j] === "\r" ? 2 : 1;
}
// else: no newline — leave j as-is, lang stays ""
const closeIdx = content.indexOf("```", j);
if (closeIdx === -1) {
blocks.push({ type: "code", lang, value: content.slice(j), streaming: true });
i = content.length;
} else {
blocks.push({ type: "code", lang, value: content.slice(j, closeIdx).replace(/\n$/, ""), streaming: false });
i = closeIdx + 3;
}
}
return blocks;
}
export function Markdown({ content }) {
if (!content) return null;
const blocks = [];
const codeBlockRe = /```(\w*)\n?([\s\S]*?)```/g;
let last = 0;
let match;
while ((match = codeBlockRe.exec(content)) !== null) {
if (match.index > last) {
blocks.push({ type: "text", value: content.slice(last, match.index) });
}
blocks.push({ type: "code", lang: match[1], value: match[2] });
last = match.index + match[0].length;
}
if (last < content.length) {
blocks.push({ type: "text", value: content.slice(last) });
}
const blocks = parseBlocks(content);
return (
<div style={{ lineHeight: "1.6" }}>
{blocks.map((block, i) =>
block.type === "code" ? (
<pre key={i} style={{
block.type === "code"
? <CodeBlock key={i} lang={block.lang} value={block.value} streaming={block.streaming} />
: <TextBlock key={i} text={block.value} />
)}
</div>
);
}
function CodeBlock({ lang, value, streaming }) {
const [copied, setCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText(value.trimEnd()).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
};
return (
<div style={{
background: "#0d0d0d",
border: "1px solid #2a2a2a",
borderRadius: "6px",
margin: "0.5rem 0",
overflow: "hidden",
}}>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0.3rem 0.75rem",
background: "#161616",
borderBottom: "1px solid #2a2a2a",
}}>
<span style={{ fontSize: "0.75rem", color: "#666", fontFamily: "monospace" }}>
{lang || "code"}
{streaming && <span style={{ color: "#444", marginLeft: "0.5rem" }}></span>}
</span>
{!streaming && (
<button onClick={copy} style={{
background: "transparent",
border: "none",
color: copied ? "#4caf50" : "#555",
cursor: "pointer",
fontSize: "0.75rem",
padding: "0.1rem 0.3rem",
}}>
{copied ? "Copied!" : "Copy"}
</button>
)}
</div>
<pre style={{
padding: "0.75rem 1rem",
overflowX: "auto",
fontSize: "0.85rem",
lineHeight: "1.5",
margin: "0.5rem 0",
margin: 0,
fontFamily: "monospace",
}}>
<code>{block.value.trimEnd()}</code>
<code>{value.trimEnd()}</code>
</pre>
) : (
<TextBlock key={i} text={block.value} />
)
)}
</div>
);
}
@@ -50,7 +125,6 @@ function TextBlock({ text }) {
while (i < lines.length) {
const line = lines[i];
// Heading
const hMatch = line.match(/^(#{1,3})\s+(.+)/);
if (hMatch) {
const level = hMatch[1].length;
@@ -60,11 +134,9 @@ function TextBlock({ text }) {
{inlineMarkdown(hMatch[2])}
</div>
);
i++;
continue;
i++; continue;
}
// Unordered list
if (/^[-*]\s+/.test(line)) {
const items = [];
while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
@@ -75,7 +147,6 @@ function TextBlock({ text }) {
continue;
}
// Ordered list
if (/^\d+\.\s+/.test(line)) {
const items = [];
while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
@@ -86,21 +157,16 @@ function TextBlock({ text }) {
continue;
}
// Horizontal rule
if (/^---+$/.test(line.trim())) {
elements.push(<hr key={i} style={{ border: "none", borderTop: "1px solid #333", margin: "0.5rem 0" }} />);
i++;
continue;
i++; continue;
}
// Empty line → small gap
if (line.trim() === "") {
elements.push(<div key={i} style={{ height: "0.4rem" }} />);
i++;
continue;
i++; continue;
}
// Regular paragraph line
elements.push(<div key={i}>{inlineMarkdown(line)}</div>);
i++;
}
@@ -116,7 +182,6 @@ function inlineMarkdown(text) {
while ((m = re.exec(text)) !== null) {
if (m.index > last) parts.push(text.slice(last, m.index));
if (m[2] !== undefined) {
parts.push(<strong key={m.index}>{m[2]}</strong>);
} else if (m[3] !== undefined) {
Regular → Executable
+274 -20
View File
@@ -13,6 +13,28 @@ function groupBySection(items) {
return map;
}
function indentLevelOf(text) {
const m = text.match(/^( *)/);
return Math.floor((m ? m[0].length : 0) / 2);
}
// Collapse a section's items into top-level "groups": each group is a parent
// (indent 0) plus its consecutive indented children.
function computeGroups(sectionItems) {
const groups = [];
const groupIndexById = {};
for (const item of sectionItems) {
const lvl = indentLevelOf(item.text);
if (lvl === 0 || groups.length === 0) {
groups.push([item]);
} else {
groups[groups.length - 1].push(item);
}
groupIndexById[item.id] = groups.length - 1;
}
return { groups, groupIndexById };
}
export function Memory() {
const [items, setItems] = useState([]);
const [editing, setEditing] = useState(null); // { id, section, text } or null
@@ -20,6 +42,8 @@ export function Memory() {
const [newSection, setNewSection] = useState("");
const [showNewSection, setShowNewSection] = useState(false);
const [message, setMessage] = useState("");
const [draggingId, setDraggingId] = useState(null);
const [dragOver, setDragOver] = useState(null); // { id, position: "before"|"after" }
const loadItems = useCallback(async () => {
try {
@@ -36,22 +60,30 @@ export function Memory() {
const flash = (msg) => { setMessage(msg); setTimeout(() => setMessage(""), 3000); };
const saveNew = async (section, text) => {
if (!text.trim()) return;
// Split on newlines so a pasted list becomes one memory per line (leading
// spaces kept for nesting). Direct write — no LLM curator, saves every line.
const lines = text.split("\n").map(l => l.replace(/\s+$/, "")).filter(l => l.trim());
if (!lines.length) return;
let ok = 0;
for (const line of lines) {
const res = await fetch(`${API_BASE}/memory`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ section, text: text.trim() }),
body: JSON.stringify({ section, text: line }),
});
if (res.ok) { loadItems(); setAdding(null); flash("Saved."); }
if (res.ok) ok++;
}
if (ok) { loadItems(); setAdding(null); flash(ok === 1 ? "Saved." : `Saved ${ok} items.`); }
else flash("Save failed.");
};
const saveEdit = async (id, section, text) => {
if (!text.trim()) return;
const cleaned = text.replace(/\s+$/, "");
if (!cleaned.trim()) return;
const res = await fetch(`${API_BASE}/memory/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ section, text: text.trim() }),
body: JSON.stringify({ section, text: cleaned }),
});
if (res.ok) { loadItems(); setEditing(null); flash("Updated."); }
else flash("Update failed.");
@@ -64,6 +96,154 @@ export function Memory() {
else flash("Delete failed.");
};
const reorderSection = async (section, ids) => {
try {
const res = await fetch(`${API_BASE}/memory/reorder`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ section, ids }),
});
const result = await res.json();
if (!res.ok || result.ok === false) {
throw new Error("Memory reorder failed");
}
loadItems();
} catch (err) {
setMessage(err.message);
}
};
const moveItem = (id, direction) => {
const item = items.find(i => i.id === id);
if (!item) return;
const section = item.section || "General";
const sectionItems = items.filter(i => (i.section || "General") === section);
const { groups, groupIndexById } = computeGroups(sectionItems);
const lvl = indentLevelOf(item.text);
if (lvl === 0) {
// Swap whole group with previous/next group
const gIdx = groupIndexById[id];
const tIdx = direction === "up" ? gIdx - 1 : gIdx + 1;
if (tIdx < 0 || tIdx >= groups.length) return;
const newGroups = [...groups];
[newGroups[gIdx], newGroups[tIdx]] = [newGroups[tIdx], newGroups[gIdx]];
reorderSection(section, newGroups.flat().map(i => i.id));
} else {
// Swap child with previous/next sibling within same group; never past the parent
const gIdx = groupIndexById[id];
const group = groups[gIdx];
const localIdx = group.findIndex(i => i.id === id);
const tLocal = direction === "up" ? localIdx - 1 : localIdx + 1;
if (tLocal <= 0 || tLocal >= group.length) return; // tLocal === 0 is the parent
const newGroup = [...group];
[newGroup[localIdx], newGroup[tLocal]] = [newGroup[tLocal], newGroup[localIdx]];
const newGroups = [...groups];
newGroups[gIdx] = newGroup;
reorderSection(section, newGroups.flat().map(i => i.id));
}
};
const onItemDragStart = (e, item) => {
setDraggingId(item.id);
e.dataTransfer.effectAllowed = "move";
try { e.dataTransfer.setData("text/plain", item.id); } catch { /* some browsers restrict */ }
};
const onItemDragOver = (e, targetItem, section) => {
if (!draggingId || draggingId === targetItem.id) return;
const dragged = items.find(i => i.id === draggingId);
if (!dragged || (dragged.section || "General") !== section) return;
const sectionItems = items.filter(i => (i.section || "General") === section);
const { groups, groupIndexById } = computeGroups(sectionItems);
const draggedLevel = indentLevelOf(dragged.text);
const targetGroupIdx = groupIndexById[targetItem.id];
const draggedGroupIdx = groupIndexById[dragged.id];
const rect = e.currentTarget.getBoundingClientRect();
const inUpperHalf = e.clientY < rect.top + rect.height / 2;
if (draggedLevel === 0) {
// Top-level drag: snap to group boundaries; ignore hover over own group
if (targetGroupIdx === draggedGroupIdx) {
if (dragOver) setDragOver(null);
return;
}
e.preventDefault();
e.dataTransfer.dropEffect = "move";
const targetGroup = groups[targetGroupIdx];
const isTargetParent = targetItem.id === targetGroup[0].id;
const position = (isTargetParent && inUpperHalf) ? "before" : "after";
if (dragOver?.kind !== "group" || dragOver?.section !== section || dragOver?.groupIdx !== targetGroupIdx || dragOver?.position !== position) {
setDragOver({ kind: "group", section, groupIdx: targetGroupIdx, position });
}
} else {
// Child drag: confined to its own parent group
if (targetGroupIdx !== draggedGroupIdx) {
if (dragOver) setDragOver(null);
return;
}
e.preventDefault();
e.dataTransfer.dropEffect = "move";
const targetGroup = groups[targetGroupIdx];
const isTargetParent = targetItem.id === targetGroup[0].id;
let position = inUpperHalf ? "before" : "after";
if (isTargetParent) position = "after"; // child can't go before its parent
if (dragOver?.kind !== "item" || dragOver?.section !== section || dragOver?.id !== targetItem.id || dragOver?.position !== position) {
setDragOver({ kind: "item", section, id: targetItem.id, position });
}
}
};
const onItemDrop = (e, targetItem, section) => {
e.preventDefault();
const dragged = items.find(i => i.id === draggingId);
if (!dragged || dragged.id === targetItem.id || (dragged.section || "General") !== section || !dragOver) {
setDraggingId(null);
setDragOver(null);
return;
}
const sectionItems = items.filter(i => (i.section || "General") === section);
const { groups, groupIndexById } = computeGroups(sectionItems);
const draggedLevel = indentLevelOf(dragged.text);
let block, anchorId, after;
if (draggedLevel === 0 && dragOver.kind === "group") {
block = groups[groupIndexById[dragged.id]];
const targetGroup = groups[dragOver.groupIdx];
anchorId = dragOver.position === "before" ? targetGroup[0].id : targetGroup[targetGroup.length - 1].id;
after = dragOver.position === "after";
} else if (draggedLevel > 0 && dragOver.kind === "item") {
block = [dragged];
anchorId = dragOver.id;
after = dragOver.position === "after";
} else {
setDraggingId(null);
setDragOver(null);
return;
}
const blockIds = new Set(block.map(b => b.id));
const remainder = sectionItems.filter(i => !blockIds.has(i.id));
let insertIdx = remainder.findIndex(i => i.id === anchorId);
if (insertIdx < 0) {
setDraggingId(null);
setDragOver(null);
return;
}
if (after) insertIdx += 1;
const newOrder = [...remainder.slice(0, insertIdx), ...block, ...remainder.slice(insertIdx)];
setDraggingId(null);
setDragOver(null);
reorderSection(section, newOrder.map(i => i.id));
};
const onItemDragEnd = () => {
setDraggingId(null);
setDragOver(null);
};
const addSection = () => {
const sec = newSection.trim();
if (!sec) return;
@@ -98,7 +278,7 @@ export function Memory() {
});
return (
<div style={{ flexGrow: 1, maxWidth: "860px" }}>
<div style={{ flexGrow: 1 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1.25rem" }}>
<div>
<h2 style={{ margin: 0, fontSize: "1.1rem" }}>Memory</h2>
@@ -148,9 +328,49 @@ export function Memory() {
</div>
{/* Bullet items */}
{(() => {
const sectionItems = grouped[section] || [];
const { groups, groupIndexById } = computeGroups(sectionItems);
return (
<div style={{ paddingLeft: "1rem", borderLeft: "2px solid #222" }}>
{(grouped[section] || []).map(item => (
<div key={item.id} style={{ marginBottom: "0.4rem" }}>
{sectionItems.map((item) => {
const indentMatch = item.text.match(/^( *)(.*)$/s);
const indentLevel = indentMatch ? Math.floor(indentMatch[1].length / 2) : 0;
const displayText = indentMatch ? indentMatch[2] : item.text;
const gIdx = groupIndexById[item.id];
const group = groups[gIdx];
const localIdx = group.findIndex(i => i.id === item.id);
const isParentRow = localIdx === 0;
const isLastInGroup = localIdx === group.length - 1;
const arrowUpDisabled = indentLevel === 0 ? gIdx === 0 : localIdx <= 1;
const arrowDownDisabled = indentLevel === 0 ? gIdx === groups.length - 1 : isLastInGroup;
const isDragging = draggingId === item.id;
let showLineBefore = false, showLineAfter = false;
if (dragOver && dragOver.section === section) {
if (dragOver.kind === "group" && dragOver.groupIdx === gIdx) {
if (dragOver.position === "before" && isParentRow) showLineBefore = true;
if (dragOver.position === "after" && isLastInGroup) showLineAfter = true;
} else if (dragOver.kind === "item" && dragOver.id === item.id) {
if (dragOver.position === "before") showLineBefore = true;
else showLineAfter = true;
}
}
return (
<div
key={item.id}
draggable={!editing}
onDragStart={(e) => onItemDragStart(e, item)}
onDragOver={(e) => onItemDragOver(e, item, section)}
onDrop={(e) => onItemDrop(e, item, section)}
onDragEnd={onItemDragEnd}
style={{
marginBottom: "0.4rem",
paddingLeft: `${indentLevel * 1.5}rem`,
borderTop: showLineBefore ? "2px solid #4db8ff" : "2px solid transparent",
borderBottom: showLineAfter ? "2px solid #4db8ff" : "2px solid transparent",
opacity: isDragging ? 0.4 : 1,
}}
>
{editing?.id === item.id ? (
<EditRow
initial={editing.text}
@@ -160,17 +380,29 @@ export function Memory() {
btnStyle={btnStyle}
/>
) : (
<div style={{ display: "flex", alignItems: "flex-start", gap: "0.5rem", group: true }}>
<div
onDoubleClick={() => setEditing({ id: item.id, section, text: item.text })}
title="Drag to reorder · double-click to edit"
style={{ display: "flex", alignItems: "flex-start", gap: "0.5rem", cursor: "grab" }}
>
<span style={{ color: "#aaa", marginTop: "0.1rem", flexShrink: 0 }}>-</span>
<span style={{ color: "#ddd", fontSize: "0.9rem", flexGrow: 1 }}>{item.text}</span>
<span style={{ color: "#ddd", fontSize: "0.9rem", flexGrow: 1 }}>{displayText}</span>
<div style={{ display: "flex", gap: "0.3rem", flexShrink: 0, opacity: 0.4 }}
onMouseEnter={e => e.currentTarget.style.opacity = 1}
onMouseLeave={e => e.currentTarget.style.opacity = 0.4}
>
<button
onClick={() => setEditing({ id: item.id, section, text: item.text })}
style={{ ...btnStyle("#333"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
>edit</button>
onClick={() => moveItem(item.id, "up")}
disabled={arrowUpDisabled}
title="Move up"
style={{ ...btnStyle("#333"), fontSize: "0.72rem", padding: "0.2rem 0.4rem", opacity: arrowUpDisabled ? 0.3 : 1, cursor: arrowUpDisabled ? "default" : "pointer" }}
></button>
<button
onClick={() => moveItem(item.id, "down")}
disabled={arrowDownDisabled}
title="Move down"
style={{ ...btnStyle("#333"), fontSize: "0.72rem", padding: "0.2rem 0.4rem", opacity: arrowDownDisabled ? 0.3 : 1, cursor: arrowDownDisabled ? "default" : "pointer" }}
></button>
<button
onClick={() => deleteItem(item.id)}
style={{ ...btnStyle("#5a1a1a"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
@@ -179,7 +411,8 @@ export function Memory() {
</div>
)}
</div>
))}
);
})}
{/* Inline add form */}
{adding?.section === section && (
@@ -191,6 +424,8 @@ export function Memory() {
/>
)}
</div>
);
})()}
</div>
))}
@@ -203,19 +438,34 @@ export function Memory() {
);
}
function handleIndentKey(e, setText) {
if (e.key !== "Tab") return false;
e.preventDefault();
if (e.shiftKey) {
setText(t => t.startsWith(" ") ? t.slice(2) : t);
} else {
setText(t => " " + t);
}
return true;
}
function AddRow({ onSave, onCancel, inputStyle, btnStyle }) {
const [text, setText] = useState("");
return (
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginTop: "0.4rem" }}>
<span style={{ color: "#555", flexShrink: 0 }}>-</span>
<input
<textarea
autoFocus
type="text"
placeholder="New item..."
rows={1}
placeholder="New item… (paste multiple lines to add several at once)"
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={e => { if (e.key === "Enter") onSave(text); if (e.key === "Escape") onCancel(); }}
style={{ ...inputStyle }}
onKeyDown={e => {
if (handleIndentKey(e, setText)) return;
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); onSave(text); }
if (e.key === "Escape") onCancel();
}}
style={{ ...inputStyle, resize: "vertical", fontFamily: "inherit", minHeight: "2.1rem" }}
/>
<button onClick={() => onSave(text)} style={btnStyle()}>Save</button>
<button onClick={onCancel} style={btnStyle("#444")}></button>
@@ -233,7 +483,11 @@ function EditRow({ initial, onSave, onCancel, inputStyle, btnStyle }) {
type="text"
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={e => { if (e.key === "Enter") onSave(text); if (e.key === "Escape") onCancel(); }}
onKeyDown={e => {
if (handleIndentKey(e, setText)) return;
if (e.key === "Enter") onSave(text);
if (e.key === "Escape") onCancel();
}}
style={{ ...inputStyle }}
/>
<button onClick={() => onSave(text)} style={btnStyle()}>Save</button>
Regular → Executable
+13 -12
View File
@@ -1,28 +1,25 @@
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { API_BASE } from "./config";
export function Models({ status }) {
export function Models({ onPullStateChange }) {
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [pulling, setPulling] = useState(false);
const [modelName, setModelName] = useState("");
const [pullProgress, setPullProgress] = useState("");
const [ollamaStatus, setOllamaStatus] = useState("checking");
const checkOllamaStatus = async () => {
const checkOllamaStatus = useCallback(async () => {
try {
const response = await fetch(`${API_BASE}/ollama/status`);
const data = await response.json();
setOllamaStatus(data.status);
return data.status === "running";
} catch {
setOllamaStatus("unavailable");
return false;
}
};
}, []);
const loadModels = async () => {
const loadModels = useCallback(async () => {
setLoading(true);
setError("");
try {
@@ -35,14 +32,15 @@ export function Models({ status }) {
const response = await fetch(`${API_BASE}/models/details`);
if (!response.ok) throw new Error("Failed to fetch models");
const data = await response.json();
setModels(data.models || []);
// Embedding models (nomic-embed-text) are infrastructure — hide them.
setModels((data.models || []).filter((m) => !m.name.toLowerCase().includes("embed")));
} catch (err) {
setError(`Error loading models: ${err.message}`);
setModels([]);
} finally {
setLoading(false);
}
};
}, [checkOllamaStatus]);
const pullModel = async () => {
if (!modelName.trim()) {
@@ -51,6 +49,7 @@ export function Models({ status }) {
}
setPulling(true);
onPullStateChange?.(true);
setError("");
setPullProgress("");
@@ -99,6 +98,7 @@ export function Models({ status }) {
setError(`Error pulling model: ${err.message}`);
} finally {
setPulling(false);
onPullStateChange?.(false);
}
};
@@ -123,8 +123,9 @@ export function Models({ status }) {
useEffect(() => {
loadModels();
const interval = setInterval(loadModels, 30000);
return () => clearInterval(interval);
}, []);
}, [loadModels]);
return (
<div style={{ display: "flex", flexDirection: "column", height: "100%", flexGrow: 1 }}>
@@ -214,7 +215,7 @@ export function Models({ status }) {
)}
</div>
{/* Models List */}
{/* Local Models List */}
<div style={{ flexGrow: 1, overflowY: "auto", paddingRight: "0.5rem" }}>
<h3 style={{ margin: "0 0 1rem 0", fontSize: "0.95rem", color: "#bbb" }}>
Available Models ({models.length})
Regular → Executable
+2 -2
View File
@@ -1,7 +1,7 @@
import { useEffect, useState, useCallback, useRef } from "react";
import { API_BASE } from "./config";
export function Playbook({ status }) {
export function Playbook() {
const [playbooks, setPlaybooks] = useState([]);
const [selectedId, setSelectedId] = useState(null);
const [form, setForm] = useState({ title: "", goal: "", instructions: "", tags: "" });
@@ -29,6 +29,7 @@ export function Playbook({ status }) {
}
}, []);
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch on mount; setPlaybooks runs after await, not a synchronous cascading render
useEffect(() => { loadPlaybooks(); }, [loadPlaybooks]);
const safeString = (v) => (v == null ? "" : String(v));
@@ -184,7 +185,6 @@ export function Playbook({ status }) {
// Optimistic UI update — reorder within filtered set, keep unfiltered playbooks intact
setPlaybooks(prev => {
const filteredSet = new Set(currentOrder);
const unfiltered = prev.filter(p => !filteredSet.has(String(p.id)));
const filteredMap = Object.fromEntries(prev.filter(p => filteredSet.has(String(p.id))).map(p => [String(p.id), p]));
const reorderedFiltered = newFilteredOrder.map(id => filteredMap[id]).filter(Boolean);
// Merge: place unfiltered items back in their original relative positions
Regular → Executable
+381 -1
View File
@@ -6,6 +6,10 @@ const DEFAULTS = {
temperature: 0.7,
system_prompt: "",
timeout: 120,
gpu_offload: -1, // -1 = Auto; 0100 = percent of layers forced onto the GPU
memory_model: "mistral:latest", // curator model for memory extraction
memory_gpu_offload: 0, // curator offload; 0 = all CPU/RAM (safe on small GPUs)
memory_merge_threshold: 0.80, // 0 = off; else update near-duplicate facts in place
};
export function Settings() {
@@ -13,11 +17,25 @@ export function Settings() {
const [saved, setSaved] = useState(false);
const [error, setError] = useState("");
// Icon branding state
const [iconApps, setIconApps] = useState([]);
const [iconFilter, setIconFilter] = useState("");
const [iconFrac, setIconFrac] = useState(0.60);
const [iconRing, setIconRing] = useState(false);
const [selectedApps, setSelectedApps] = useState([]);
const [brandQueue, setBrandQueue] = useState([]);
const [isBranding, setIsBranding] = useState(false);
useEffect(() => {
fetch(`${API_BASE}/settings`)
.then(r => r.ok ? r.json() : null)
.then(d => { if (d) setForm(f => ({ ...f, ...d })); })
.catch(() => {});
fetch(`${API_BASE}/icons/apps`)
.then(r => r.ok ? r.json() : null)
.then(d => { if (d) setIconApps(d.apps || []); })
.catch(() => {});
}, []);
const update = (key, value) => setForm(f => ({ ...f, [key]: value }));
@@ -41,6 +59,73 @@ export function Settings() {
const reset = () => setForm(DEFAULTS);
// Icon branding logic
const toggleAppSelect = (app) => {
setSelectedApps(prev => {
const exists = prev.find(a => a.icon_name === app.icon_name);
if (exists) return prev.filter(a => a.icon_name !== app.icon_name);
return [...prev, app];
});
};
const removeFromQueue = (icon_name) => {
setSelectedApps(prev => prev.filter(a => a.icon_name !== icon_name));
};
const brandAll = async () => {
if (!selectedApps.length || isBranding) return;
setIsBranding(true);
const queue = selectedApps.map(a => ({ ...a, status: "pending" }));
setBrandQueue(queue);
for (let i = 0; i < queue.length; i++) {
const app = queue[i];
setBrandQueue(prev => prev.map((a, idx) => idx === i ? { ...a, status: "running" } : a));
try {
const r = await fetch(`${API_BASE}/icons/brand`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
src_path: app.icon_path,
output_name: app.icon_name,
frac: iconFrac,
nexus_ring: iconRing,
reload: false,
}),
});
if (!r.ok) throw new Error(await r.text());
setBrandQueue(prev => prev.map((a, idx) => idx === i ? { ...a, status: "done" } : a));
} catch (e) {
setBrandQueue(prev => prev.map((a, idx) => idx === i ? { ...a, status: "error", err: e.message } : a));
}
}
// Apply cache once after all are done
await fetch(`${API_BASE}/icons/apply`, { method: "POST" }).catch(() => {});
// Mark already_branded in the app list
setIconApps(prev => prev.map(a => {
if (selectedApps.find(s => s.icon_name === a.icon_name)) {
return { ...a, already_branded: true };
}
return a;
}));
setIsBranding(false);
};
const filteredApps = iconApps.filter(a =>
a.name.toLowerCase().includes(iconFilter.toLowerCase())
);
const statusIcon = (status) => {
if (status === "pending") return <span style={{ color: "#666" }}></span>;
if (status === "running") return <span style={{ color: "#8cc63f" }}></span>;
if (status === "done") return <span style={{ color: "#8cc63f" }}></span>;
if (status === "error") return <span style={{ color: "#f44336" }}></span>;
return null;
};
const inputStyle = {
width: "100%",
padding: "0.6rem 0.75rem",
@@ -69,8 +154,10 @@ export function Settings() {
marginBottom: "1rem",
};
const displayQueue = isBranding ? brandQueue : selectedApps;
return (
<div style={{ maxWidth: "640px" }}>
<div style={{ maxWidth: "720px" }}>
<h2 style={{ margin: "0 0 1.25rem", fontSize: "1.1rem", color: "#eee" }}>Settings</h2>
{/* Generation */}
@@ -99,6 +186,140 @@ export function Settings() {
<span>2.0 creative</span>
</div>
</div>
<div style={{ marginTop: "1.25rem" }}>
<label style={labelStyle}>
GPU Offload
<span style={{ float: "right", color: "#b040c0", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
{form.gpu_offload < 0 ? "Auto" : `${form.gpu_offload}%`}
</span>
</label>
<input
type="range"
min="0"
max="100"
step="5"
value={form.gpu_offload < 0 ? 100 : form.gpu_offload}
disabled={form.gpu_offload < 0}
onChange={e => update("gpu_offload", parseInt(e.target.value))}
style={{ width: "100%", accentColor: "#b040c0", opacity: form.gpu_offload < 0 ? 0.4 : 1 }}
/>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.72rem", color: "#555", marginTop: "0.2rem" }}>
<span>0% all CPU</span>
<span>100% all GPU</span>
</div>
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", userSelect: "none", marginTop: "0.6rem" }}>
<input
type="checkbox"
checked={form.gpu_offload < 0}
onChange={e => update("gpu_offload", e.target.checked ? -1 : 50)}
style={{ accentColor: "#8cc63f", width: "16px", height: "16px" }}
/>
<span style={{ fontSize: "0.82rem", color: form.gpu_offload < 0 ? "#8cc63f" : "#888" }}>
Auto fit as many layers as VRAM allows (recommended)
</span>
</label>
<p style={{ margin: "0.4rem 0 0", fontSize: "0.72rem", color: "#555" }}>
Lower values push more of the model onto the CPU slower, but frees VRAM and reduces GPU load. Layer count is resolved per model.
</p>
</div>
</div>
{/* Memory Curator */}
<div style={sectionStyle}>
<h3 style={{ margin: "0 0 1rem", fontSize: "0.95rem", color: "#bbb" }}>Memory Curator</h3>
<p style={{ margin: "0 0 1rem", fontSize: "0.75rem", color: "#555" }}>
The model that reviews each exchange and saves new facts to memory, in the background after every reply.
</p>
<div style={{ marginBottom: "1rem" }}>
<label style={labelStyle}>Curator Model</label>
<input
type="text"
value={form.memory_model}
onChange={e => update("memory_model", e.target.value)}
placeholder="mistral:latest (blank = same model as chat)"
style={inputStyle}
/>
<p style={{ margin: "0.35rem 0 0", fontSize: "0.75rem", color: "#555" }}>
A dedicated model like <code>mistral:latest</code> extracts facts more reliably than a small chat model.
</p>
</div>
<div>
<label style={labelStyle}>
Curator GPU Offload
<span style={{ float: "right", color: "#b040c0", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
{form.memory_gpu_offload < 0 ? "Auto" : `${form.memory_gpu_offload}%`}
</span>
</label>
<input
type="range"
min="0"
max="100"
step="5"
value={form.memory_gpu_offload < 0 ? 100 : form.memory_gpu_offload}
disabled={form.memory_gpu_offload < 0}
onChange={e => update("memory_gpu_offload", parseInt(e.target.value))}
style={{ width: "100%", accentColor: "#b040c0", opacity: form.memory_gpu_offload < 0 ? 0.4 : 1 }}
/>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.72rem", color: "#555", marginTop: "0.2rem" }}>
<span>0% all CPU/RAM</span>
<span>100% all GPU</span>
</div>
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", userSelect: "none", marginTop: "0.6rem" }}>
<input
type="checkbox"
checked={form.memory_gpu_offload < 0}
onChange={e => update("memory_gpu_offload", e.target.checked ? -1 : 0)}
style={{ accentColor: "#8cc63f", width: "16px", height: "16px" }}
/>
<span style={{ fontSize: "0.82rem", color: form.memory_gpu_offload < 0 ? "#8cc63f" : "#888" }}>
Auto let Ollama fit layers to spare VRAM
</span>
</label>
<p style={{ margin: "0.4rem 0 0", fontSize: "0.72rem", color: "#555" }}>
Keep at <strong>0% (all CPU/RAM)</strong> on a small GPU so the curator doesn't evict the chat model on a ~4&nbsp;GB GPU that thrash makes extraction time out and silently save nothing. Raise it only if you have spare VRAM.
</p>
</div>
<div style={{ marginTop: "1.25rem" }}>
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", userSelect: "none" }}>
<input
type="checkbox"
checked={form.memory_merge_threshold > 0}
onChange={e => update("memory_merge_threshold", e.target.checked ? 0.80 : 0)}
style={{ accentColor: "#b040c0", width: "16px", height: "16px" }}
/>
<span style={{ fontSize: "0.85rem", color: "#ccc", fontWeight: 600 }}>Merge similar facts (edit in place)</span>
</label>
<p style={{ margin: "0.4rem 0 0.7rem", fontSize: "0.72rem", color: "#555" }}>
When a newly extracted fact closely matches an existing one, update that entry instead of adding a duplicate. Uses embeddings.
</p>
{form.memory_merge_threshold > 0 && (
<>
<label style={labelStyle}>
Merge threshold
<span style={{ float: "right", color: "#b040c0", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
{Number(form.memory_merge_threshold).toFixed(2)}
</span>
</label>
<input
type="range"
min="0.60"
max="0.98"
step="0.01"
value={form.memory_merge_threshold}
onChange={e => update("memory_merge_threshold", parseFloat(e.target.value))}
style={{ width: "100%", accentColor: "#b040c0" }}
/>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.72rem", color: "#555", marginTop: "0.2rem" }}>
<span>0.60 merges more</span>
<span>0.98 only near-identical</span>
</div>
</>
)}
</div>
</div>
{/* Behaviour */}
@@ -143,6 +364,165 @@ export function Settings() {
</div>
</div>
{/* Icon Branding */}
<div style={sectionStyle}>
<h3 style={{ margin: "0 0 0.75rem", fontSize: "0.95rem", color: "#bbb" }}>App Icon Branding</h3>
<p style={{ margin: "0 0 1rem", fontSize: "0.78rem", color: "#555" }}>
Select apps on the left to composite their icon onto the NexusOS tile.
</p>
{/* Controls row */}
<div style={{ display: "flex", gap: "1.5rem", marginBottom: "1rem", alignItems: "center", flexWrap: "wrap" }}>
<div style={{ flex: "1 1 180px" }}>
<label style={labelStyle}>
Logo size
<span style={{ float: "right", color: "#b040c0", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
{Math.round(iconFrac * 100)}%
</span>
</label>
<input
type="range"
min="0.30"
max="0.85"
step="0.05"
value={iconFrac}
onChange={e => setIconFrac(parseFloat(e.target.value))}
style={{ width: "100%", accentColor: "#b040c0" }}
/>
</div>
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", userSelect: "none" }}>
<input
type="checkbox"
checked={iconRing}
onChange={e => setIconRing(e.target.checked)}
style={{ accentColor: "#8cc63f", width: "16px", height: "16px" }}
/>
<span style={{ fontSize: "0.85rem", color: iconRing ? "#8cc63f" : "#888" }}>Nexus ring</span>
</label>
</div>
{/* Shuttle */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
{/* Left: app list */}
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<input
type="text"
placeholder="Filter apps…"
value={iconFilter}
onChange={e => setIconFilter(e.target.value)}
style={{ ...inputStyle, padding: "0.4rem 0.6rem", fontSize: "0.82rem" }}
/>
<div style={{ height: "280px", overflowY: "auto", background: "#0d0d0d", border: "1px solid #2a2a2a", borderRadius: "8px" }}>
{filteredApps.length === 0 && (
<div style={{ padding: "1rem", color: "#444", fontSize: "0.82rem", textAlign: "center" }}>
{iconApps.length === 0 ? "Loading…" : "No apps match"}
</div>
)}
{filteredApps.map(app => {
const isSelected = !!selectedApps.find(a => a.icon_name === app.icon_name);
return (
<div
key={app.icon_name}
onClick={() => !isBranding && !isSelected && app.icon_path && toggleAppSelect(app)}
style={{
display: "flex",
alignItems: "center",
gap: "0.5rem",
padding: "0.35rem 0.6rem",
cursor: isBranding || isSelected || !app.icon_path ? "default" : "pointer",
background: isSelected ? "rgba(176,64,192,0.15)" : "transparent",
borderLeft: isSelected ? "2px solid #b040c0" : "2px solid transparent",
opacity: !app.icon_path ? 0.35 : 1,
}}
onMouseEnter={e => { if (!isSelected && app.icon_path && !isBranding) e.currentTarget.style.background = "rgba(255,255,255,0.04)"; }}
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = "transparent"; }}
>
{app.icon_path ? (
<img
src={`${API_BASE}/icons/image?path=${encodeURIComponent(app.icon_path)}`}
alt=""
style={{ width: "22px", height: "22px", objectFit: "contain", flexShrink: 0 }}
onError={e => { e.target.style.display = "none"; }}
/>
) : (
<div style={{ width: "22px", height: "22px", background: "#1a1a1a", borderRadius: "4px", flexShrink: 0 }} />
)}
<span style={{ fontSize: "0.82rem", color: "#ccc", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{app.name}
</span>
{app.already_branded && (
<span style={{ marginLeft: "auto", color: "#8cc63f", fontSize: "0.72rem", flexShrink: 0 }}></span>
)}
</div>
);
})}
</div>
</div>
{/* Right: queue */}
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<div style={{ fontSize: "0.8rem", color: "#555", padding: "0.4rem 0", minHeight: "2rem" }}>
{isBranding ? `Branding ${brandQueue.length} app${brandQueue.length !== 1 ? "s" : ""}` : `${selectedApps.length} selected`}
</div>
<div style={{ height: "280px", overflowY: "auto", background: "#0d0d0d", border: "1px solid #2a2a2a", borderRadius: "8px" }}>
{displayQueue.length === 0 && (
<div style={{ padding: "1rem", color: "#333", fontSize: "0.82rem", textAlign: "center" }}>
Click apps to add
</div>
)}
{displayQueue.map(app => (
<div
key={app.icon_name}
style={{ display: "flex", alignItems: "center", gap: "0.5rem", padding: "0.35rem 0.6rem" }}
>
{isBranding ? (
<span style={{ fontSize: "1rem", width: "18px", textAlign: "center", flexShrink: 0 }}>
{statusIcon(app.status)}
</span>
) : (
<button
onClick={() => removeFromQueue(app.icon_name)}
style={{ background: "transparent", border: "none", color: "#555", cursor: "pointer", fontSize: "0.9rem", padding: "0 2px", lineHeight: 1, flexShrink: 0 }}
onMouseEnter={e => e.target.style.color = "#f44336"}
onMouseLeave={e => e.target.style.color = "#555"}
>
×
</button>
)}
<span style={{ fontSize: "0.82rem", color: isBranding && app.status === "done" ? "#8cc63f" : "#ccc", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{app.name}
</span>
</div>
))}
</div>
</div>
</div>
{/* Brand button */}
<div style={{ marginTop: "0.75rem", display: "flex", alignItems: "center", gap: "0.75rem" }}>
<button
onClick={brandAll}
disabled={!selectedApps.length || isBranding}
style={{
padding: "0.6rem 1.25rem",
background: selectedApps.length && !isBranding ? "#5e0066" : "#1a1a1a",
color: selectedApps.length && !isBranding ? "#eee" : "#444",
border: `1px solid ${selectedApps.length && !isBranding ? "#b040c0" : "#2a2a2a"}`,
borderRadius: "8px",
cursor: selectedApps.length && !isBranding ? "pointer" : "default",
fontSize: "0.88rem",
fontWeight: 500,
transition: "background 0.15s",
}}
>
{isBranding ? "Branding…" : `Brand ${selectedApps.length || ""} App${selectedApps.length !== 1 ? "s" : ""}`}
</button>
{!isBranding && brandQueue.length > 0 && brandQueue.every(a => a.status === "done") && (
<span style={{ color: "#8cc63f", fontSize: "0.85rem" }}>Done panel reloaded</span>
)}
</div>
</div>
{/* Actions */}
<div style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
<button
View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Regular → Executable
View File
Regular → Executable
+6 -4
View File
@@ -1,3 +1,8 @@
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
:root {
--text: #6b6375;
--text-h: #08060d;
@@ -55,11 +60,8 @@ body {
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
width: 100%;
text-align: left;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
Regular → Executable
View File
Regular → Executable
+3
View File
@@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
// host: true binds 0.0.0.0 so the Windows browser can reach the dev server
// through WSL2 (loopback-only binding isn't reliably forwarded out of WSL).
server: { host: true },
})