feat: floor plan map, LQI edge coloring, zigbee path highlighting, eslint/test fixes, vite audit fix
This commit is contained in:
Generated
+199
-184
@@ -963,6 +963,40 @@
|
|||||||
"@noble/ciphers": "^1.0.0"
|
"@noble/ciphers": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emnapi/core": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||||
@@ -1916,6 +1950,25 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"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.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@noble/ciphers": {
|
"node_modules/@noble/ciphers": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
|
||||||
@@ -2429,9 +2482,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
|
||||||
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
|
"integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -2443,9 +2496,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz",
|
||||||
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
|
"integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2457,9 +2510,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz",
|
||||||
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
|
"integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2471,9 +2524,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz",
|
||||||
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
|
"integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2485,9 +2538,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz",
|
||||||
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
|
"integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2499,9 +2552,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz",
|
||||||
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
|
"integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2513,16 +2566,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz",
|
||||||
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
|
"integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2530,16 +2580,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz",
|
||||||
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
|
"integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2547,16 +2594,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz",
|
||||||
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
|
"integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2564,16 +2608,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz",
|
||||||
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
|
"integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2581,16 +2622,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz",
|
||||||
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
|
"integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2598,16 +2636,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz",
|
||||||
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
|
"integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2615,16 +2650,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz",
|
||||||
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
|
"integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2632,16 +2664,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz",
|
||||||
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
|
"integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2649,16 +2678,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz",
|
||||||
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
|
"integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2666,16 +2692,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz",
|
||||||
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
|
"integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2683,16 +2706,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz",
|
||||||
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
|
"integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2700,16 +2720,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz",
|
||||||
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
|
"integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2717,16 +2734,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz",
|
||||||
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
|
"integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2734,9 +2748,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz",
|
||||||
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
|
"integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2748,9 +2762,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz",
|
||||||
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
|
"integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2762,9 +2776,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz",
|
||||||
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
|
"integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2776,9 +2790,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz",
|
||||||
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
|
"integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -2790,9 +2804,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz",
|
||||||
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
|
"integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2804,9 +2818,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz",
|
||||||
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
|
"integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2858,6 +2872,13 @@
|
|||||||
"tailwindcss": "4.2.2"
|
"tailwindcss": "4.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
|
||||||
@@ -2975,9 +2996,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2995,9 +3013,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3015,9 +3030,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3035,9 +3047,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3126,6 +3135,13 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@testing-library/dom": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "10.4.1",
|
"version": "10.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
@@ -3263,6 +3279,17 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||||
|
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/aria-query": {
|
"node_modules/@types/aria-query": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
@@ -3391,9 +3418,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -6945,9 +6972,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6969,9 +6993,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -6993,9 +7014,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7017,9 +7035,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -7455,9 +7470,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -7888,9 +7903,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.14",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -7907,7 +7922,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -8243,13 +8258,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.60.1",
|
"version": "4.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz",
|
||||||
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
|
"integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.9"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
@@ -8259,31 +8274,31 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.60.1",
|
"@rollup/rollup-android-arm-eabi": "4.62.0",
|
||||||
"@rollup/rollup-android-arm64": "4.60.1",
|
"@rollup/rollup-android-arm64": "4.62.0",
|
||||||
"@rollup/rollup-darwin-arm64": "4.60.1",
|
"@rollup/rollup-darwin-arm64": "4.62.0",
|
||||||
"@rollup/rollup-darwin-x64": "4.60.1",
|
"@rollup/rollup-darwin-x64": "4.62.0",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.60.1",
|
"@rollup/rollup-freebsd-arm64": "4.62.0",
|
||||||
"@rollup/rollup-freebsd-x64": "4.60.1",
|
"@rollup/rollup-freebsd-x64": "4.62.0",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.62.0",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
|
"@rollup/rollup-linux-arm-musleabihf": "4.62.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
|
"@rollup/rollup-linux-arm64-gnu": "4.62.0",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.60.1",
|
"@rollup/rollup-linux-arm64-musl": "4.62.0",
|
||||||
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
|
"@rollup/rollup-linux-loong64-gnu": "4.62.0",
|
||||||
"@rollup/rollup-linux-loong64-musl": "4.60.1",
|
"@rollup/rollup-linux-loong64-musl": "4.62.0",
|
||||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
|
"@rollup/rollup-linux-ppc64-gnu": "4.62.0",
|
||||||
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
|
"@rollup/rollup-linux-ppc64-musl": "4.62.0",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
|
"@rollup/rollup-linux-riscv64-gnu": "4.62.0",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
|
"@rollup/rollup-linux-riscv64-musl": "4.62.0",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
|
"@rollup/rollup-linux-s390x-gnu": "4.62.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.60.1",
|
"@rollup/rollup-linux-x64-gnu": "4.62.0",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.60.1",
|
"@rollup/rollup-linux-x64-musl": "4.62.0",
|
||||||
"@rollup/rollup-openbsd-x64": "4.60.1",
|
"@rollup/rollup-openbsd-x64": "4.62.0",
|
||||||
"@rollup/rollup-openharmony-arm64": "4.60.1",
|
"@rollup/rollup-openharmony-arm64": "4.62.0",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
|
"@rollup/rollup-win32-arm64-msvc": "4.62.0",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
|
"@rollup/rollup-win32-ia32-msvc": "4.62.0",
|
||||||
"@rollup/rollup-win32-x64-gnu": "4.60.1",
|
"@rollup/rollup-win32-x64-gnu": "4.62.0",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.60.1",
|
"@rollup/rollup-win32-x64-msvc": "4.62.0",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -8856,9 +8871,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.2.2",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
|
||||||
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
|
"integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -8900,14 +8915,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3"
|
"picomatch": "^4.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
|
|||||||
+43
-7
@@ -26,6 +26,7 @@ import { SettingsModal } from '@/components/modals/SettingsModal'
|
|||||||
import { ZigbeeImportModal } from '@/components/zigbee/ZigbeeImportModal'
|
import { ZigbeeImportModal } from '@/components/zigbee/ZigbeeImportModal'
|
||||||
import { ZwaveImportModal } from '@/components/zwave/ZwaveImportModal'
|
import { ZwaveImportModal } from '@/components/zwave/ZwaveImportModal'
|
||||||
import { GroupRectModal, type GroupRectFormData } from '@/components/modals/GroupRectModal'
|
import { GroupRectModal, type GroupRectFormData } from '@/components/modals/GroupRectModal'
|
||||||
|
import { FloorMapModal } from '@/components/modals/FloorMapModal'
|
||||||
import { TextModal, type TextFormData } from '@/components/modals/TextModal'
|
import { TextModal, type TextFormData } from '@/components/modals/TextModal'
|
||||||
import { ThemeModal } from '@/components/modals/ThemeModal'
|
import { ThemeModal } from '@/components/modals/ThemeModal'
|
||||||
import { SearchModal } from '@/components/modals/SearchModal'
|
import { SearchModal } from '@/components/modals/SearchModal'
|
||||||
@@ -41,14 +42,14 @@ import { canvasApi, designsApi, liveviewApi } from '@/api/client'
|
|||||||
import * as standaloneStorage from '@/utils/standaloneStorage'
|
import * as standaloneStorage from '@/utils/standaloneStorage'
|
||||||
import { demoNodes, demoEdges } from '@/utils/demoData'
|
import { demoNodes, demoEdges } from '@/utils/demoData'
|
||||||
import { useStatusPolling } from '@/hooks/useStatusPolling'
|
import { useStatusPolling } from '@/hooks/useStatusPolling'
|
||||||
import type { NodeData, EdgeData, CustomStyleDef } from '@/types'
|
import type { NodeData, EdgeData, CustomStyleDef, FloorMapConfig } from '@/types'
|
||||||
import type { ZigbeeNode, ZigbeeEdge } from '@/components/zigbee/types'
|
import type { ZigbeeNode, ZigbeeEdge } from '@/components/zigbee/types'
|
||||||
import type { ZwaveNode, ZwaveEdge } from '@/components/zwave/types'
|
import type { ZwaveNode, ZwaveEdge } from '@/components/zwave/types'
|
||||||
|
|
||||||
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup, addToContainer } = useCanvasStore()
|
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup, addToContainer, floorMap, setFloorMap } = useCanvasStore()
|
||||||
const canvasRef = useRef<HTMLDivElement>(null)
|
const canvasRef = useRef<HTMLDivElement>(null)
|
||||||
const { isAuthenticated } = useAuthStore()
|
const { isAuthenticated } = useAuthStore()
|
||||||
const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore()
|
const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore()
|
||||||
@@ -82,6 +83,7 @@ export default function App() {
|
|||||||
const [exportModalOpen, setExportModalOpen] = useState(false)
|
const [exportModalOpen, setExportModalOpen] = useState(false)
|
||||||
const [zigbeeImportOpen, setZigbeeImportOpen] = useState(false)
|
const [zigbeeImportOpen, setZigbeeImportOpen] = useState(false)
|
||||||
const [zwaveImportOpen, setZwaveImportOpen] = useState(false)
|
const [zwaveImportOpen, setZwaveImportOpen] = useState(false)
|
||||||
|
const [floorMapModalOpen, setFloorMapModalOpen] = useState(false)
|
||||||
|
|
||||||
// Declare handleSave before the Ctrl+S effect so it is in scope.
|
// Declare handleSave before the Ctrl+S effect so it is in scope.
|
||||||
// Returns true on success, false on failure — the design-switch effect relies
|
// Returns true on success, false on failure — the design-switch effect relies
|
||||||
@@ -91,14 +93,16 @@ export default function App() {
|
|||||||
const saveDesignId = designIdOverride ?? activeDesignId
|
const saveDesignId = designIdOverride ?? activeDesignId
|
||||||
if (STANDALONE) {
|
if (STANDALONE) {
|
||||||
if (!saveDesignId) return false
|
if (!saveDesignId) return false
|
||||||
standaloneStorage.saveCanvas(saveDesignId, { nodes, edges, theme_id: activeTheme, custom_style: customStyle })
|
standaloneStorage.saveCanvas(saveDesignId, { nodes, edges, theme_id: activeTheme, custom_style: customStyle, floorMap })
|
||||||
markSaved()
|
markSaved()
|
||||||
toast.success('Canvas saved')
|
toast.success('Canvas saved')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const nodesToSave = nodes.map(serializeNode)
|
const nodesToSave = nodes.map(serializeNode)
|
||||||
const edgesToSave = edges.map(serializeEdge)
|
const edgesToSave = edges.map(serializeEdge)
|
||||||
await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport: { theme_id: activeTheme }, custom_style: customStyle, design_id: saveDesignId })
|
const viewport: Record<string, unknown> = { theme_id: activeTheme }
|
||||||
|
if (floorMap) viewport.floor_map = floorMap
|
||||||
|
await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport, custom_style: customStyle, design_id: saveDesignId })
|
||||||
markSaved()
|
markSaved()
|
||||||
toast.success('Canvas saved')
|
toast.success('Canvas saved')
|
||||||
return true
|
return true
|
||||||
@@ -106,7 +110,7 @@ export default function App() {
|
|||||||
toast.error('Save failed')
|
toast.error('Save failed')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}, [nodes, edges, markSaved, activeTheme, customStyle, activeDesignId])
|
}, [nodes, edges, markSaved, activeTheme, customStyle, activeDesignId, floorMap])
|
||||||
|
|
||||||
// Keep a ref so the keydown handler always calls the latest version
|
// Keep a ref so the keydown handler always calls the latest version
|
||||||
const handleSaveRef = useRef(handleSave)
|
const handleSaveRef = useRef(handleSave)
|
||||||
@@ -127,6 +131,8 @@ export default function App() {
|
|||||||
const savedTheme = res.data.viewport?.theme_id
|
const savedTheme = res.data.viewport?.theme_id
|
||||||
if (savedTheme) setTheme(savedTheme)
|
if (savedTheme) setTheme(savedTheme)
|
||||||
if (res.data.custom_style) setCustomStyle(res.data.custom_style as CustomStyleDef)
|
if (res.data.custom_style) setCustomStyle(res.data.custom_style as CustomStyleDef)
|
||||||
|
const savedFloorMap = res.data.viewport?.floor_map as FloorMapConfig | undefined
|
||||||
|
if (savedFloorMap) setFloorMap(savedFloorMap)
|
||||||
loadCanvas(rfNodes, rfEdges)
|
loadCanvas(rfNodes, rfEdges)
|
||||||
} else {
|
} else {
|
||||||
loadCanvas(demoNodes, demoEdges)
|
loadCanvas(demoNodes, demoEdges)
|
||||||
@@ -134,7 +140,7 @@ export default function App() {
|
|||||||
} catch {
|
} catch {
|
||||||
loadCanvas(demoNodes, demoEdges)
|
loadCanvas(demoNodes, demoEdges)
|
||||||
}
|
}
|
||||||
}, [loadCanvas, setTheme, setCustomStyle])
|
}, [loadCanvas, setTheme, setCustomStyle, setFloorMap])
|
||||||
|
|
||||||
// Standalone counterpart of loadCanvasFromApi — reads a design's canvas from
|
// Standalone counterpart of loadCanvasFromApi — reads a design's canvas from
|
||||||
// localStorage, falling back to the demo canvas when it has never been saved.
|
// localStorage, falling back to the demo canvas when it has never been saved.
|
||||||
@@ -143,11 +149,12 @@ export default function App() {
|
|||||||
if (saved && saved.nodes.length > 0) {
|
if (saved && saved.nodes.length > 0) {
|
||||||
if (saved.theme_id) setTheme(saved.theme_id)
|
if (saved.theme_id) setTheme(saved.theme_id)
|
||||||
if (saved.custom_style) setCustomStyle(saved.custom_style)
|
if (saved.custom_style) setCustomStyle(saved.custom_style)
|
||||||
|
if (saved.floorMap) setFloorMap(saved.floorMap)
|
||||||
loadCanvas(saved.nodes, saved.edges)
|
loadCanvas(saved.nodes, saved.edges)
|
||||||
} else {
|
} else {
|
||||||
loadCanvas(demoNodes, demoEdges)
|
loadCanvas(demoNodes, demoEdges)
|
||||||
}
|
}
|
||||||
}, [loadCanvas, setTheme, setCustomStyle])
|
}, [loadCanvas, setTheme, setCustomStyle, setFloorMap])
|
||||||
|
|
||||||
const loadDesignsAndCanvas = useCallback(async () => {
|
const loadDesignsAndCanvas = useCallback(async () => {
|
||||||
if (STANDALONE) {
|
if (STANDALONE) {
|
||||||
@@ -518,6 +525,25 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}, [activeDesignId])
|
}, [activeDesignId])
|
||||||
|
|
||||||
|
const handleFloorMapSubmit = useCallback((config: FloorMapConfig) => {
|
||||||
|
snapshotHistory()
|
||||||
|
if (floorMap) {
|
||||||
|
// Preserve position from existing config when editing
|
||||||
|
setFloorMap({ ...floorMap, ...config })
|
||||||
|
} else {
|
||||||
|
setFloorMap(config)
|
||||||
|
}
|
||||||
|
setFloorMapModalOpen(false)
|
||||||
|
toast.success(floorMap ? 'Floor plan updated' : 'Floor plan imported')
|
||||||
|
}, [floorMap, setFloorMap, snapshotHistory])
|
||||||
|
|
||||||
|
const handleFloorMapRemove = useCallback(() => {
|
||||||
|
snapshotHistory()
|
||||||
|
setFloorMap(null)
|
||||||
|
setFloorMapModalOpen(false)
|
||||||
|
toast.success('Floor plan removed')
|
||||||
|
}, [setFloorMap, snapshotHistory])
|
||||||
|
|
||||||
const handleExport = useCallback(() => {
|
const handleExport = useCallback(() => {
|
||||||
const el = canvasRef.current?.querySelector<HTMLElement>('.react-flow')
|
const el = canvasRef.current?.querySelector<HTMLElement>('.react-flow')
|
||||||
if (!el) { toast.error('Canvas not ready'); return }
|
if (!el) { toast.error('Canvas not ready'); return }
|
||||||
@@ -697,6 +723,7 @@ export default function App() {
|
|||||||
onScan={() => setScanConfigOpen(true)}
|
onScan={() => setScanConfigOpen(true)}
|
||||||
onZigbeeImport={() => setZigbeeImportOpen(true)}
|
onZigbeeImport={() => setZigbeeImportOpen(true)}
|
||||||
onZwaveImport={() => setZwaveImportOpen(true)}
|
onZwaveImport={() => setZwaveImportOpen(true)}
|
||||||
|
onFloorMap={() => setFloorMapModalOpen(true)}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onOpenSettings={() => setSettingsOpen(true)}
|
onOpenSettings={() => setSettingsOpen(true)}
|
||||||
onOpenHistory={() => setScanHistoryOpen(true)}
|
onOpenHistory={() => setScanHistoryOpen(true)}
|
||||||
@@ -833,6 +860,15 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<FloorMapModal
|
||||||
|
key={floorMapModalOpen ? 'floor-map-open' : 'floor-map-closed'}
|
||||||
|
open={floorMapModalOpen}
|
||||||
|
onClose={() => setFloorMapModalOpen(false)}
|
||||||
|
onSubmit={handleFloorMapSubmit}
|
||||||
|
onRemove={handleFloorMapRemove}
|
||||||
|
initial={floorMap}
|
||||||
|
/>
|
||||||
|
|
||||||
<GroupRectModal
|
<GroupRectModal
|
||||||
open={addGroupRectOpen}
|
open={addGroupRectOpen}
|
||||||
onClose={() => setAddGroupRectOpen(false)}
|
onClose={() => setAddGroupRectOpen(false)}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { nodeTypes } from './nodes/nodeTypes'
|
|||||||
import { edgeTypes } from './edges/edgeTypes'
|
import { edgeTypes } from './edges/edgeTypes'
|
||||||
import { SearchBar } from './SearchBar'
|
import { SearchBar } from './SearchBar'
|
||||||
import { AlignmentGuides } from './AlignmentGuides'
|
import { AlignmentGuides } from './AlignmentGuides'
|
||||||
|
import { FloorMapLayer } from './FloorMapLayer'
|
||||||
import { useAlignmentGuides } from '@/hooks/useAlignmentGuides'
|
import { useAlignmentGuides } from '@/hooks/useAlignmentGuides'
|
||||||
import { setViewportCenterProjector } from '@/utils/viewportCenter'
|
import { setViewportCenterProjector } from '@/utils/viewportCenter'
|
||||||
import type { NodeData, EdgeData } from '@/types'
|
import type { NodeData, EdgeData } from '@/types'
|
||||||
@@ -199,6 +200,7 @@ export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, o
|
|||||||
size={1}
|
size={1}
|
||||||
color={theme.colors.canvasDotColor}
|
color={theme.colors.canvasDotColor}
|
||||||
/>
|
/>
|
||||||
|
<FloorMapLayer screenToFlowPosition={screenToFlowPosition} />
|
||||||
<SearchBar onOpenPending={onOpenPending} />
|
<SearchBar onOpenPending={onOpenPending} />
|
||||||
<AlignmentGuides guides={guides} />
|
<AlignmentGuides guides={guides} />
|
||||||
<Controls>
|
<Controls>
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { useCallback, useRef } from 'react'
|
||||||
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
|
|
||||||
|
interface FloorMapLayerProps {
|
||||||
|
screenToFlowPosition: (pos: { x: number; y: number }) => { x: number; y: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResizeState {
|
||||||
|
startMouseX: number
|
||||||
|
startMouseY: number
|
||||||
|
startX: number
|
||||||
|
startY: number
|
||||||
|
startW: number
|
||||||
|
startH: number
|
||||||
|
edges: Set<'n' | 's' | 'e' | 'w'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FloorMapLayer({ screenToFlowPosition }: FloorMapLayerProps) {
|
||||||
|
const floorMap = useCanvasStore((s) => s.floorMap)
|
||||||
|
const updateFloorMap = useCanvasStore((s) => s.updateFloorMap)
|
||||||
|
|
||||||
|
const resizeRef = useRef<ResizeState | null>(null)
|
||||||
|
|
||||||
|
const onDragStart = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (!floorMap) return
|
||||||
|
e.stopPropagation()
|
||||||
|
const startX = e.clientX
|
||||||
|
const startY = e.clientY
|
||||||
|
const origPosX = floorMap.posX
|
||||||
|
const origPosY = floorMap.posY
|
||||||
|
|
||||||
|
const onMove = (ev: MouseEvent) => {
|
||||||
|
const start = screenToFlowPosition({ x: startX, y: startY })
|
||||||
|
const cur = screenToFlowPosition({ x: ev.clientX, y: ev.clientY })
|
||||||
|
updateFloorMap({ posX: origPosX + (cur.x - start.x), posY: origPosY + (cur.y - start.y) })
|
||||||
|
}
|
||||||
|
const onUp = () => {
|
||||||
|
window.removeEventListener('mousemove', onMove)
|
||||||
|
window.removeEventListener('mouseup', onUp)
|
||||||
|
}
|
||||||
|
window.addEventListener('mousemove', onMove)
|
||||||
|
window.addEventListener('mouseup', onUp)
|
||||||
|
}, [floorMap, updateFloorMap, screenToFlowPosition])
|
||||||
|
|
||||||
|
const onResizeStart = useCallback((e: React.MouseEvent, edges: Set<'n' | 's' | 'e' | 'w'>) => {
|
||||||
|
if (!floorMap) return
|
||||||
|
e.stopPropagation()
|
||||||
|
resizeRef.current = {
|
||||||
|
startMouseX: e.clientX,
|
||||||
|
startMouseY: e.clientY,
|
||||||
|
startX: floorMap.posX,
|
||||||
|
startY: floorMap.posY,
|
||||||
|
startW: floorMap.width,
|
||||||
|
startH: floorMap.height,
|
||||||
|
edges,
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMove = (ev: MouseEvent) => {
|
||||||
|
const rs = resizeRef.current
|
||||||
|
if (!rs) return
|
||||||
|
const start = screenToFlowPosition({ x: rs.startMouseX, y: rs.startMouseY })
|
||||||
|
const cur = screenToFlowPosition({ x: ev.clientX, y: ev.clientY })
|
||||||
|
const dx = cur.x - start.x
|
||||||
|
const dy = cur.y - start.y
|
||||||
|
let x = rs.startX, y = rs.startY, w = rs.startW, h = rs.startH
|
||||||
|
if (rs.edges.has('w')) { x += dx; w -= dx }
|
||||||
|
if (rs.edges.has('e')) w += dx
|
||||||
|
if (rs.edges.has('n')) { y += dy; h -= dy }
|
||||||
|
if (rs.edges.has('s')) h += dy
|
||||||
|
const MIN = 80
|
||||||
|
if (w < MIN) {
|
||||||
|
if (rs.edges.has('w')) x = rs.startX + rs.startW - MIN
|
||||||
|
w = MIN
|
||||||
|
}
|
||||||
|
if (h < MIN) {
|
||||||
|
if (rs.edges.has('n')) y = rs.startY + rs.startH - MIN
|
||||||
|
h = MIN
|
||||||
|
}
|
||||||
|
updateFloorMap({ posX: x, posY: y, width: w, height: h })
|
||||||
|
}
|
||||||
|
const onUp = () => {
|
||||||
|
resizeRef.current = null
|
||||||
|
window.removeEventListener('mousemove', onMove)
|
||||||
|
window.removeEventListener('mouseup', onUp)
|
||||||
|
}
|
||||||
|
window.addEventListener('mousemove', onMove)
|
||||||
|
window.addEventListener('mouseup', onUp)
|
||||||
|
}, [floorMap, updateFloorMap, screenToFlowPosition])
|
||||||
|
|
||||||
|
if (!floorMap || !floorMap.enabled) return null
|
||||||
|
|
||||||
|
const { imageData, posX, posY, width, height, opacity, locked } = floorMap
|
||||||
|
|
||||||
|
const hs: React.CSSProperties = {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
background: '#00d4ff',
|
||||||
|
border: '2px solid #0d1117',
|
||||||
|
borderRadius: 2,
|
||||||
|
zIndex: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: posX,
|
||||||
|
top: posY,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
opacity,
|
||||||
|
zIndex: -1,
|
||||||
|
pointerEvents: locked ? 'none' : 'auto',
|
||||||
|
cursor: locked ? 'default' : 'move',
|
||||||
|
}}
|
||||||
|
onMouseDown={locked ? undefined : onDragStart}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={imageData}
|
||||||
|
alt="Floor plan"
|
||||||
|
draggable={false}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'fill',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{!locked && (
|
||||||
|
<>
|
||||||
|
<div style={{ ...hs, cursor: 'nw-resize', top: -5, left: -5 }} onMouseDown={(e) => onResizeStart(e, new Set(['n','w']))} />
|
||||||
|
<div style={{ ...hs, cursor: 'n-resize', top: -5, left: '50%', marginLeft: -5 }} onMouseDown={(e) => onResizeStart(e, new Set(['n']))} />
|
||||||
|
<div style={{ ...hs, cursor: 'ne-resize', top: -5, right: -5 }} onMouseDown={(e) => onResizeStart(e, new Set(['n','e']))} />
|
||||||
|
<div style={{ ...hs, cursor: 'e-resize', top: '50%', marginTop: -5, right: -5 }} onMouseDown={(e) => onResizeStart(e, new Set(['e']))} />
|
||||||
|
<div style={{ ...hs, cursor: 'se-resize', bottom: -5, right: -5 }} onMouseDown={(e) => onResizeStart(e, new Set(['s','e']))} />
|
||||||
|
<div style={{ ...hs, cursor: 's-resize', bottom: -5, left: '50%', marginLeft: -5 }} onMouseDown={(e) => onResizeStart(e, new Set(['s']))} />
|
||||||
|
<div style={{ ...hs, cursor: 'sw-resize', bottom: -5, left: -5 }} onMouseDown={(e) => onResizeStart(e, new Set(['s','w']))} />
|
||||||
|
<div style={{ ...hs, cursor: 'w-resize', top: '50%', marginTop: -5, left: -5 }} onMouseDown={(e) => onResizeStart(e, new Set(['w']))} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type EdgeProps,
|
type EdgeProps,
|
||||||
type Edge,
|
type Edge,
|
||||||
} from '@xyflow/react'
|
} from '@xyflow/react'
|
||||||
import type { EdgeData, EdgeType, Waypoint } from '@/types'
|
import type { EdgeData, EdgeType, NodeData, Waypoint } from '@/types'
|
||||||
import { useThemeStore } from '@/stores/themeStore'
|
import { useThemeStore } from '@/stores/themeStore'
|
||||||
import { useCanvasStore } from '@/stores/canvasStore'
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
import { THEMES } from '@/utils/themes'
|
import { THEMES } from '@/utils/themes'
|
||||||
@@ -22,6 +22,13 @@ function getVlanColor(vlanId?: number): string {
|
|||||||
return VLAN_COLORS[vlanId % VLAN_COLORS.length]
|
return VLAN_COLORS[vlanId % VLAN_COLORS.length]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLqiColor(lqi: number): string {
|
||||||
|
if (lqi >= 200) return '#00cc00'
|
||||||
|
if (lqi >= 150) return '#88cc00'
|
||||||
|
if (lqi >= 100) return '#ffaa00'
|
||||||
|
return '#ff3300'
|
||||||
|
}
|
||||||
|
|
||||||
// ── Waypoint drag handle ─────────────────────────────────────────────────────
|
// ── Waypoint drag handle ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface WaypointHandleProps {
|
interface WaypointHandleProps {
|
||||||
@@ -289,7 +296,8 @@ export function HomelableEdge({ id, source, target, sourceHandleId, targetHandle
|
|||||||
const activeTheme = useThemeStore((s) => s.activeTheme)
|
const activeTheme = useThemeStore((s) => s.activeTheme)
|
||||||
const theme = THEMES[activeTheme]
|
const theme = THEMES[activeTheme]
|
||||||
const sourceType = useStore((s) => s.nodeLookup.get(source)?.type)
|
const sourceType = useStore((s) => s.nodeLookup.get(source)?.type)
|
||||||
const targetType = useStore((s) => s.nodeLookup.get(target)?.type)
|
const targetNode = useStore((s) => s.nodeLookup.get(target))
|
||||||
|
const targetType = targetNode?.type
|
||||||
const isBidirectional = sourceType === 'proxmox' && targetType === 'proxmox'
|
const isBidirectional = sourceType === 'proxmox' && targetType === 'proxmox'
|
||||||
|
|
||||||
const waypoints: Waypoint[] = Array.isArray(data?.waypoints) && data.waypoints.length > 0
|
const waypoints: Waypoint[] = Array.isArray(data?.waypoints) && data.waypoints.length > 0
|
||||||
@@ -315,6 +323,8 @@ export function HomelableEdge({ id, source, target, sourceHandleId, targetHandle
|
|||||||
|
|
||||||
const edgeType: EdgeType = data?.type ?? 'ethernet'
|
const edgeType: EdgeType = data?.type ?? 'ethernet'
|
||||||
const edgeColors = theme.colors.edgeColors
|
const edgeColors = theme.colors.edgeColors
|
||||||
|
const highlightedPath = useCanvasStore((s) => s.highlightedPath)
|
||||||
|
const isHighlighted = highlightedPath.includes(id)
|
||||||
|
|
||||||
const BASE_STYLES: Record<EdgeType, React.CSSProperties> = {
|
const BASE_STYLES: Record<EdgeType, React.CSSProperties> = {
|
||||||
ethernet: { stroke: edgeColors.ethernet, strokeWidth: 2 },
|
ethernet: { stroke: edgeColors.ethernet, strokeWidth: 2 },
|
||||||
@@ -328,15 +338,36 @@ export function HomelableEdge({ id, source, target, sourceHandleId, targetHandle
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customColor = data?.custom_color as string | undefined
|
const customColor = data?.custom_color as string | undefined
|
||||||
const strokeColor: string = selected
|
|
||||||
|
// Derive LQI color from the target node's properties for iot edges
|
||||||
|
const lqiColor = (edgeType === 'iot' && targetNode?.data?.properties)
|
||||||
|
? (() => {
|
||||||
|
const props = (targetNode.data as NodeData).properties ?? []
|
||||||
|
const lqiProp = props.find(
|
||||||
|
(p: { key: string; value: string }) => p.key === 'LQI'
|
||||||
|
)
|
||||||
|
if (lqiProp) {
|
||||||
|
const lqi = parseInt(lqiProp.value, 10)
|
||||||
|
if (!isNaN(lqi)) return getLqiColor(lqi)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})()
|
||||||
|
: null
|
||||||
|
|
||||||
|
const pathHighlightColor = '#00d4ff'
|
||||||
|
const strokeColor: string = isHighlighted
|
||||||
|
? pathHighlightColor
|
||||||
|
: selected
|
||||||
? theme.colors.edgeSelectedColor
|
? theme.colors.edgeSelectedColor
|
||||||
: customColor
|
: lqiColor
|
||||||
|
?? customColor
|
||||||
?? (edgeType === 'vlan' ? getVlanColor(data?.vlan_id as number | undefined) : (BASE_STYLES[edgeType].stroke as string ?? edgeColors.ethernet))
|
?? (edgeType === 'vlan' ? getVlanColor(data?.vlan_id as number | undefined) : (BASE_STYLES[edgeType].stroke as string ?? edgeColors.ethernet))
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
...BASE_STYLES[edgeType],
|
...BASE_STYLES[edgeType],
|
||||||
...(edgeType === 'vlan' ? { stroke: getVlanColor(data?.vlan_id as number | undefined) } : {}),
|
...(edgeType === 'vlan' ? { stroke: getVlanColor(data?.vlan_id as number | undefined) } : {}),
|
||||||
...(customColor ? { stroke: customColor } : {}),
|
...(customColor ? { stroke: customColor } : {}),
|
||||||
|
...(isHighlighted ? { stroke: pathHighlightColor, strokeWidth: 3, filter: `drop-shadow(0 0 6px ${pathHighlightColor}aa)` } : {}),
|
||||||
...(selected ? { stroke: theme.colors.edgeSelectedColor, filter: `drop-shadow(0 0 4px ${theme.colors.edgeSelectedColor}88)` } : {}),
|
...(selected ? { stroke: theme.colors.edgeSelectedColor, filter: `drop-shadow(0 0 4px ${theme.colors.edgeSelectedColor}88)` } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useState, useRef } from 'react'
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import type { FloorMapConfig } from '@/types'
|
||||||
|
|
||||||
|
interface FloorMapModalProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: (config: FloorMapConfig) => void
|
||||||
|
onRemove: () => void
|
||||||
|
initial: FloorMapConfig | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FloorMapModal({ open, onClose, onSubmit, onRemove, initial }: FloorMapModalProps) {
|
||||||
|
const [imageData, setImageData] = useState(initial?.imageData ?? '')
|
||||||
|
const [width, setWidth] = useState(initial?.width ?? 800)
|
||||||
|
const [height, setHeight] = useState(initial?.height ?? 600)
|
||||||
|
const [opacity, setOpacity] = useState(initial?.opacity ?? 0.8)
|
||||||
|
const [locked, setLocked] = useState(initial?.locked ?? false)
|
||||||
|
const [enabled, setEnabled] = useState(initial?.enabled ?? true)
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const handleFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
const dataUrl = ev.target?.result as string
|
||||||
|
setImageData(dataUrl)
|
||||||
|
const img = new Image()
|
||||||
|
img.onload = () => {
|
||||||
|
setWidth(img.naturalWidth)
|
||||||
|
setHeight(img.naturalHeight)
|
||||||
|
}
|
||||||
|
img.src = dataUrl
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!imageData) return
|
||||||
|
onSubmit({
|
||||||
|
imageData,
|
||||||
|
posX: initial?.posX ?? 0,
|
||||||
|
posY: initial?.posY ?? 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
opacity,
|
||||||
|
locked,
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasImage = !!imageData
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose() }}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{initial ? 'Edit Floor Plan' : 'Import Floor Plan'}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 py-2">
|
||||||
|
{!hasImage ? (
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center gap-2 border-2 border-dashed border-[#30363d] rounded-lg p-8 cursor-pointer hover:border-[#00d4ff]/50 transition-colors"
|
||||||
|
onClick={() => fileRef.current?.click()}
|
||||||
|
>
|
||||||
|
<input ref={fileRef} type="file" accept="image/png,image/jpeg,image/webp" className="hidden" onChange={handleFile} />
|
||||||
|
<span className="text-muted-foreground text-sm">Click to select a floor plan image</span>
|
||||||
|
<span className="text-muted-foreground/50 text-xs">PNG, JPEG or WebP</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="relative rounded-lg overflow-hidden border border-[#30363d]" style={{ maxHeight: 200 }}>
|
||||||
|
<img src={imageData} alt="Floor plan preview" className="w-full h-full object-contain" style={{ opacity }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button size="sm" variant="secondary" className="cursor-pointer" onClick={() => fileRef.current?.click()}>
|
||||||
|
Replace Image
|
||||||
|
</Button>
|
||||||
|
<input ref={fileRef} type="file" accept="image/png,image/jpeg,image.webp" className="hidden" onChange={handleFile} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">Width (px)</Label>
|
||||||
|
<Input type="number" value={width} onChange={(e) => setWidth(Math.max(80, Number(e.target.value)))} className="bg-[#21262d] border-[#30363d] text-xs h-8" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">Height (px)</Label>
|
||||||
|
<Input type="number" value={height} onChange={(e) => setHeight(Math.max(80, Number(e.target.value)))} className="bg-[#21262d] border-[#30363d] text-xs h-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">Opacity: {Math.round(opacity * 100)}%</Label>
|
||||||
|
<input
|
||||||
|
type="range" min="0.05" max="1" step="0.05" value={opacity}
|
||||||
|
onChange={(e) => setOpacity(Number(e.target.value))}
|
||||||
|
className="w-full accent-[#00d4ff]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" checked={locked} onChange={(e) => setLocked(e.target.checked)} className="accent-[#00d4ff] w-3.5 h-3.5" />
|
||||||
|
<span className="text-xs text-muted-foreground">Lock position & size</span>
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" checked={enabled} onChange={(e) => setEnabled(e.target.checked)} className="accent-[#00d4ff] w-3.5 h-3.5" />
|
||||||
|
<span className="text-xs text-muted-foreground">Show on canvas</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex items-center justify-between sm:justify-between">
|
||||||
|
{hasImage && initial && (
|
||||||
|
<Button size="sm" variant="destructive" className="cursor-pointer" onClick={onRemove}>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2 ml-auto">
|
||||||
|
<Button size="sm" variant="ghost" className="cursor-pointer" onClick={onClose}>Cancel</Button>
|
||||||
|
<Button size="sm" className="bg-[#00d4ff] text-[#0d1117] hover:bg-[#00d4ff]/90 cursor-pointer" disabled={!hasImage} onClick={handleSubmit}>
|
||||||
|
{initial ? 'Apply' : 'Import'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createElement, useRef, useState } from 'react'
|
import { createElement, useEffect, useRef, useState } from 'react'
|
||||||
import { X, Edit, Trash2, ExternalLink, Plus, Pencil, Layers, Ungroup, Eye, EyeOff } from 'lucide-react'
|
import { X, Edit, Trash2, ExternalLink, Plus, Pencil, Layers, Ungroup, Eye, EyeOff } from 'lucide-react'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@@ -9,6 +9,8 @@ import { getServiceUrl } from '@/utils/serviceUrl'
|
|||||||
import { splitIps } from '@/utils/maskIp'
|
import { splitIps } from '@/utils/maskIp'
|
||||||
import { PROPERTY_ICONS, PROPERTY_ICON_NAMES, resolvePropertyIcon } from '@/utils/propertyIcons'
|
import { PROPERTY_ICONS, PROPERTY_ICON_NAMES, resolvePropertyIcon } from '@/utils/propertyIcons'
|
||||||
import { formatTimestamp } from '@/utils/timeFormat'
|
import { formatTimestamp } from '@/utils/timeFormat'
|
||||||
|
import { findZigbeePath } from '@/utils/zigbeePathfinding'
|
||||||
|
import { isZigbeeType } from '@/utils/zigbeeProperties'
|
||||||
import type { Node } from '@xyflow/react'
|
import type { Node } from '@xyflow/react'
|
||||||
|
|
||||||
interface DetailPanelProps {
|
interface DetailPanelProps {
|
||||||
@@ -22,7 +24,7 @@ type PropForm = { key: string; value: string; icon: string | null; visible: bool
|
|||||||
const EMPTY_PROP: PropForm = { key: '', value: '', icon: null, visible: true }
|
const EMPTY_PROP: PropForm = { key: '', value: '', icon: null, visible: true }
|
||||||
|
|
||||||
export function DetailPanel({ onEdit }: DetailPanelProps) {
|
export function DetailPanel({ onEdit }: DetailPanelProps) {
|
||||||
const { nodes, selectedNodeId, selectedNodeIds, setSelectedNode, deleteNode, updateNode, snapshotHistory, createGroup, ungroup, removeFromGroup, setNodeSize } = useCanvasStore()
|
const { nodes, edges, selectedNodeId, selectedNodeIds, setSelectedNode, deleteNode, updateNode, snapshotHistory, createGroup, ungroup, removeFromGroup, setNodeSize, setHighlightedPath } = useCanvasStore()
|
||||||
const serviceStatuses = useCanvasStore((s) => s.serviceStatuses)
|
const serviceStatuses = useCanvasStore((s) => s.serviceStatuses)
|
||||||
|
|
||||||
const [addingForNode, setAddingForNode] = useState<string | null>(null)
|
const [addingForNode, setAddingForNode] = useState<string | null>(null)
|
||||||
@@ -41,6 +43,25 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
|||||||
// Multi-select panel
|
// Multi-select panel
|
||||||
const multiSelected = (selectedNodeIds ?? []).filter((id) => nodes.some((n) => n.id === id))
|
const multiSelected = (selectedNodeIds ?? []).filter((id) => nodes.some((n) => n.id === id))
|
||||||
|
|
||||||
|
// Zigbee path highlighting: when a zigbee node is selected, compute
|
||||||
|
// the optimal route to the coordinator and highlight those edges.
|
||||||
|
useEffect(() => {
|
||||||
|
const targetNode = nodes.find((n) => n.id === selectedNodeId)
|
||||||
|
if (!targetNode || !isZigbeeType(targetNode.data.type)) {
|
||||||
|
setHighlightedPath([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const coordinator = nodes.find((n) => n.data.type === 'zigbee_coordinator')
|
||||||
|
if (!coordinator) {
|
||||||
|
setHighlightedPath([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = findZigbeePath(targetNode.id, coordinator.id, nodes, edges)
|
||||||
|
setHighlightedPath(path)
|
||||||
|
}, [selectedNodeId, nodes, edges, setHighlightedPath])
|
||||||
|
|
||||||
if (multiSelected.length > 1) {
|
if (multiSelected.length > 1) {
|
||||||
return (
|
return (
|
||||||
<MultiSelectPanel
|
<MultiSelectPanel
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Square, Settings, LogOut, Network, RadioTower, Type, PlusCircle, Pencil, Trash2 } from 'lucide-react'
|
import { Plus, Save, ScanLine, ChevronLeft, ChevronRight, LayoutDashboard, Clock, EyeOff, Square, Settings, LogOut, Network, RadioTower, Type, PlusCircle, Pencil, Trash2, Image } from 'lucide-react'
|
||||||
import { Logo } from '@/components/ui/Logo'
|
import { Logo } from '@/components/ui/Logo'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import { useCanvasStore } from '@/stores/canvasStore'
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
@@ -27,13 +27,14 @@ interface SidebarProps {
|
|||||||
onScan: () => void
|
onScan: () => void
|
||||||
onZigbeeImport: () => void
|
onZigbeeImport: () => void
|
||||||
onZwaveImport: () => void
|
onZwaveImport: () => void
|
||||||
|
onFloorMap: () => void
|
||||||
onSave: () => void
|
onSave: () => void
|
||||||
onOpenSettings: () => void
|
onOpenSettings: () => void
|
||||||
onOpenHistory: () => void
|
onOpenHistory: () => void
|
||||||
onOpenPending: (deviceId?: string, status?: 'pending' | 'hidden') => void
|
onOpenPending: (deviceId?: string, status?: 'pending' | 'hidden') => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbeeImport, onZwaveImport, onSave, onOpenSettings, onOpenHistory, onOpenPending }: SidebarProps) {
|
export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbeeImport, onZwaveImport, onFloorMap, onSave, onOpenSettings, onOpenHistory, onOpenPending }: SidebarProps) {
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
const logout = useAuthStore((s) => s.logout)
|
const logout = useAuthStore((s) => s.logout)
|
||||||
const { designs, activeDesignId, setActiveDesign, addDesign, updateDesign, removeDesign } = useDesignStore()
|
const { designs, activeDesignId, setActiveDesign, addDesign, updateDesign, removeDesign } = useDesignStore()
|
||||||
@@ -76,7 +77,7 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
|
|||||||
}
|
}
|
||||||
}, [designs.length, removeDesign])
|
}, [designs.length, removeDesign])
|
||||||
|
|
||||||
const { nodes, hasUnsavedChanges } = useCanvasStore()
|
const { nodes, hasUnsavedChanges, floorMap } = useCanvasStore()
|
||||||
|
|
||||||
const networkNodes = nodes.filter((n) => n.data.type !== 'groupRect' && n.data.type !== 'text')
|
const networkNodes = nodes.filter((n) => n.data.type !== 'groupRect' && n.data.type !== 'text')
|
||||||
const onlineCount = networkNodes.filter((n) => n.data.status === 'online').length
|
const onlineCount = networkNodes.filter((n) => n.data.status === 'online').length
|
||||||
@@ -228,6 +229,7 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
|
|||||||
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
|
{!STANDALONE && <SidebarItem icon={ScanLine} label="Scan Network" collapsed={collapsed} onClick={handleScan} />}
|
||||||
{!STANDALONE && <SidebarItem icon={Network} label="Zigbee Import" collapsed={collapsed} onClick={onZigbeeImport} />}
|
{!STANDALONE && <SidebarItem icon={Network} label="Zigbee Import" collapsed={collapsed} onClick={onZigbeeImport} />}
|
||||||
{!STANDALONE && <SidebarItem icon={RadioTower} label="Z-Wave Import" collapsed={collapsed} onClick={onZwaveImport} />}
|
{!STANDALONE && <SidebarItem icon={RadioTower} label="Z-Wave Import" collapsed={collapsed} onClick={onZwaveImport} />}
|
||||||
|
<SidebarItem icon={Image} label={floorMap ? 'Edit Floor Plan' : 'Add Floor Plan'} collapsed={collapsed} onClick={onFloorMap} />
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon={Save}
|
icon={Save}
|
||||||
label="Save Canvas"
|
label="Save Canvas"
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ function setupStore(nodeData: Partial<NodeData> = {}, serviceStatuses: Record<st
|
|||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
setNodeSize: vi.fn(),
|
setNodeSize: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
serviceStatuses,
|
serviceStatuses,
|
||||||
}
|
}
|
||||||
// Support both the bare destructure call and the selector-based call.
|
// Support both the bare destructure call and the selector-based call.
|
||||||
@@ -57,6 +59,8 @@ describe('DetailPanel', () => {
|
|||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -130,6 +134,8 @@ describe('DetailPanel', () => {
|
|||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
|
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
@@ -157,6 +163,8 @@ describe('DetailPanel', () => {
|
|||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
|
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
@@ -178,6 +186,8 @@ describe('DetailPanel', () => {
|
|||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
|
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
@@ -199,6 +209,8 @@ describe('DetailPanel', () => {
|
|||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
|
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
@@ -253,6 +265,8 @@ describe('DetailPanel', () => {
|
|||||||
deleteNode: vi.fn(),
|
deleteNode: vi.fn(),
|
||||||
updateNode: vi.fn(),
|
updateNode: vi.fn(),
|
||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
fireEvent.click(screen.getByLabelText('Close panel'))
|
fireEvent.click(screen.getByLabelText('Close panel'))
|
||||||
@@ -277,6 +291,8 @@ describe('DetailPanel', () => {
|
|||||||
deleteNode,
|
deleteNode,
|
||||||
updateNode: vi.fn(),
|
updateNode: vi.fn(),
|
||||||
snapshotHistory,
|
snapshotHistory,
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
vi.spyOn(window, 'confirm').mockReturnValue(true)
|
vi.spyOn(window, 'confirm').mockReturnValue(true)
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
@@ -295,6 +311,8 @@ describe('DetailPanel', () => {
|
|||||||
deleteNode,
|
deleteNode,
|
||||||
updateNode: vi.fn(),
|
updateNode: vi.fn(),
|
||||||
snapshotHistory,
|
snapshotHistory,
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
vi.spyOn(window, 'confirm').mockReturnValue(false)
|
vi.spyOn(window, 'confirm').mockReturnValue(false)
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
@@ -326,6 +344,8 @@ describe('DetailPanel', () => {
|
|||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
// Two "Add" header buttons: first = properties, second = services
|
// Two "Add" header buttons: first = properties, second = services
|
||||||
@@ -351,6 +371,8 @@ describe('DetailPanel', () => {
|
|||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
const addHeaders = screen.getAllByText('Add')
|
const addHeaders = screen.getAllByText('Add')
|
||||||
@@ -374,6 +396,8 @@ describe('DetailPanel', () => {
|
|||||||
deleteNode: vi.fn(),
|
deleteNode: vi.fn(),
|
||||||
updateNode,
|
updateNode,
|
||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
fireEvent.click(screen.getByTitle('Remove service'))
|
fireEvent.click(screen.getByTitle('Remove service'))
|
||||||
@@ -389,6 +413,8 @@ describe('DetailPanel', () => {
|
|||||||
deleteNode: vi.fn(),
|
deleteNode: vi.fn(),
|
||||||
updateNode: vi.fn(),
|
updateNode: vi.fn(),
|
||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
expect(() => render(<DetailPanel onEdit={vi.fn()} />)).not.toThrow()
|
expect(() => render(<DetailPanel onEdit={vi.fn()} />)).not.toThrow()
|
||||||
})
|
})
|
||||||
@@ -420,6 +446,8 @@ describe('DetailPanel', () => {
|
|||||||
deleteNode: vi.fn(),
|
deleteNode: vi.fn(),
|
||||||
updateNode,
|
updateNode,
|
||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
|
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
@@ -445,6 +473,8 @@ describe('DetailPanel', () => {
|
|||||||
deleteNode: vi.fn(),
|
deleteNode: vi.fn(),
|
||||||
updateNode,
|
updateNode,
|
||||||
snapshotHistory: vi.fn(),
|
snapshotHistory: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
|
edges: [],
|
||||||
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
} as unknown as ReturnType<typeof canvasStore.useCanvasStore>)
|
||||||
|
|
||||||
render(<DetailPanel onEdit={vi.fn()} />)
|
render(<DetailPanel onEdit={vi.fn()} />)
|
||||||
@@ -608,6 +638,7 @@ describe('DetailPanel', () => {
|
|||||||
function setupSized(node: Partial<Node<NodeData>>, setNodeSize = vi.fn()) {
|
function setupSized(node: Partial<Node<NodeData>>, setNodeSize = vi.fn()) {
|
||||||
const state = {
|
const state = {
|
||||||
nodes: [{ ...makeNode({}), ...node }],
|
nodes: [{ ...makeNode({}), ...node }],
|
||||||
|
edges: [],
|
||||||
selectedNodeId: 'n1',
|
selectedNodeId: 'n1',
|
||||||
selectedNodeIds: [],
|
selectedNodeIds: [],
|
||||||
setSelectedNode: vi.fn(),
|
setSelectedNode: vi.fn(),
|
||||||
@@ -617,6 +648,7 @@ describe('DetailPanel', () => {
|
|||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
setNodeSize,
|
setNodeSize,
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
serviceStatuses: {},
|
serviceStatuses: {},
|
||||||
}
|
}
|
||||||
vi.mocked(canvasStore.useCanvasStore).mockImplementation(
|
vi.mocked(canvasStore.useCanvasStore).mockImplementation(
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ function makeGroupNode(id = 'g1', label = 'My Group', showBorder = true) {
|
|||||||
|
|
||||||
const mockStore = {
|
const mockStore = {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
selectedNodeId: null,
|
selectedNodeId: null,
|
||||||
selectedNodeIds: [],
|
selectedNodeIds: [],
|
||||||
setSelectedNode: vi.fn(),
|
setSelectedNode: vi.fn(),
|
||||||
@@ -43,6 +44,7 @@ const mockStore = {
|
|||||||
createGroup: vi.fn(),
|
createGroup: vi.fn(),
|
||||||
ungroup: vi.fn(),
|
ungroup: vi.fn(),
|
||||||
removeFromGroup: vi.fn(),
|
removeFromGroup: vi.fn(),
|
||||||
|
setHighlightedPath: vi.fn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupStore(overrides = {}) {
|
function setupStore(overrides = {}) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
applyEdgeChanges,
|
applyEdgeChanges,
|
||||||
addEdge,
|
addEdge,
|
||||||
} from '@xyflow/react'
|
} from '@xyflow/react'
|
||||||
import type { NodeData, EdgeData, NodeType, EdgeType, NodeTypeStyle, EdgeTypeStyle, CustomStyleDef, ServiceStatus } from '@/types'
|
import type { NodeData, EdgeData, NodeType, EdgeType, NodeTypeStyle, EdgeTypeStyle, CustomStyleDef, ServiceStatus, FloorMapConfig } from '@/types'
|
||||||
import { generateUUID } from '@/utils/uuid'
|
import { generateUUID } from '@/utils/uuid'
|
||||||
import { normalizeHandle, removedBottomHandleIds } from '@/utils/handleUtils'
|
import { normalizeHandle, removedBottomHandleIds } from '@/utils/handleUtils'
|
||||||
import { applyOpacity } from '@/utils/colorUtils'
|
import { applyOpacity } from '@/utils/colorUtils'
|
||||||
@@ -35,6 +35,12 @@ interface CanvasState {
|
|||||||
// Live per-service status overlay (not persisted), keyed via serviceStatusKey.
|
// Live per-service status overlay (not persisted), keyed via serviceStatusKey.
|
||||||
serviceStatuses: Record<string, ServiceStatus>
|
serviceStatuses: Record<string, ServiceStatus>
|
||||||
|
|
||||||
|
floorMap: FloorMapConfig | null
|
||||||
|
setFloorMap: (config: FloorMapConfig | null) => void
|
||||||
|
updateFloorMap: (patch: Partial<FloorMapConfig>) => void
|
||||||
|
highlightedPath: string[]
|
||||||
|
setHighlightedPath: (edgeIds: string[]) => void
|
||||||
|
|
||||||
// History
|
// History
|
||||||
past: HistoryEntry[]
|
past: HistoryEntry[]
|
||||||
future: HistoryEntry[]
|
future: HistoryEntry[]
|
||||||
@@ -98,6 +104,8 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
|||||||
hideIp: readHideIp(),
|
hideIp: readHideIp(),
|
||||||
scanEventTs: 0,
|
scanEventTs: 0,
|
||||||
serviceStatuses: {},
|
serviceStatuses: {},
|
||||||
|
floorMap: null,
|
||||||
|
highlightedPath: [],
|
||||||
fitViewPending: false,
|
fitViewPending: false,
|
||||||
|
|
||||||
past: [],
|
past: [],
|
||||||
@@ -747,6 +755,16 @@ export const useCanvasStore = create<CanvasState>((set) => ({
|
|||||||
set({ hideIp: value })
|
set({ hideIp: value })
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFloorMap: (config) => set({ floorMap: config, hasUnsavedChanges: true }),
|
||||||
|
|
||||||
|
updateFloorMap: (patch) =>
|
||||||
|
set((state) => ({
|
||||||
|
floorMap: state.floorMap ? { ...state.floorMap, ...patch } : null,
|
||||||
|
hasUnsavedChanges: true,
|
||||||
|
})),
|
||||||
|
|
||||||
|
setHighlightedPath: (edgeIds) => set({ highlightedPath: edgeIds }),
|
||||||
|
|
||||||
loadCanvas: (nodes, edges) => {
|
loadCanvas: (nodes, edges) => {
|
||||||
// React Flow requires parents before children in the array
|
// React Flow requires parents before children in the array
|
||||||
const parents = nodes.filter((n) => !n.parentId)
|
const parents = nodes.filter((n) => !n.parentId)
|
||||||
|
|||||||
@@ -252,3 +252,14 @@ export interface CustomStyleDef {
|
|||||||
nodes: Partial<Record<NodeType, NodeTypeStyle>>
|
nodes: Partial<Record<NodeType, NodeTypeStyle>>
|
||||||
edges: Partial<Record<EdgeType, EdgeTypeStyle>>
|
edges: Partial<Record<EdgeType, EdgeTypeStyle>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FloorMapConfig {
|
||||||
|
imageData: string
|
||||||
|
posX: number
|
||||||
|
posY: number
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
opacity: number
|
||||||
|
locked: boolean
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* default design on first run so existing users keep their canvas.
|
* default design on first run so existing users keep their canvas.
|
||||||
*/
|
*/
|
||||||
import type { Node, Edge } from '@xyflow/react'
|
import type { Node, Edge } from '@xyflow/react'
|
||||||
import type { Design, DesignType, NodeData, EdgeData, CustomStyleDef } from '@/types'
|
import type { Design, DesignType, NodeData, EdgeData, CustomStyleDef, FloorMapConfig } from '@/types'
|
||||||
import type { ThemeId } from '@/utils/themes'
|
import type { ThemeId } from '@/utils/themes'
|
||||||
import { generateUUID } from '@/utils/uuid'
|
import { generateUUID } from '@/utils/uuid'
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ export interface StandaloneCanvas {
|
|||||||
edges: Edge<EdgeData>[]
|
edges: Edge<EdgeData>[]
|
||||||
theme_id?: ThemeId
|
theme_id?: ThemeId
|
||||||
custom_style?: CustomStyleDef | null
|
custom_style?: CustomStyleDef | null
|
||||||
|
floorMap?: FloorMapConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
function readJSON<T>(key: string): T | null {
|
function readJSON<T>(key: string): T | null {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Node, Edge } from '@xyflow/react'
|
||||||
|
import type { NodeData, EdgeData } from '@/types'
|
||||||
|
|
||||||
|
function getNodeLqi(node: Node<NodeData>): number {
|
||||||
|
const lqiProp = node.data.properties?.find((p) => p.key === 'LQI')
|
||||||
|
if (!lqiProp) return 255
|
||||||
|
const lqi = parseInt(lqiProp.value, 10)
|
||||||
|
return isNaN(lqi) ? 255 : lqi
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run Dijkstra on the Zigbee `iot` edge subgraph from `startNodeId` to
|
||||||
|
* `targetNodeId`. Returns the ordered list of edge IDs forming the optimal
|
||||||
|
* path, or an empty array if no path exists.
|
||||||
|
*
|
||||||
|
* Edge cost = 255 - LQI, where LQI is read from the edge's target node
|
||||||
|
* properties (higher LQI = lower cost). For reverse traversal, the same
|
||||||
|
* cost is used.
|
||||||
|
*/
|
||||||
|
export function findZigbeePath(
|
||||||
|
startNodeId: string,
|
||||||
|
targetNodeId: string,
|
||||||
|
nodes: Node<NodeData>[],
|
||||||
|
edges: Edge<EdgeData>[],
|
||||||
|
): string[] {
|
||||||
|
if (startNodeId === targetNodeId) return []
|
||||||
|
|
||||||
|
const iotEdges = edges.filter((e) => (e.data?.type ?? 'ethernet') === 'iot')
|
||||||
|
if (iotEdges.length === 0) return []
|
||||||
|
|
||||||
|
const nodeMap = new Map(nodes.map((n) => [n.id, n]))
|
||||||
|
const adjacency = new Map<string, { neighbor: string; edgeId: string; cost: number }[]>()
|
||||||
|
|
||||||
|
for (const e of iotEdges) {
|
||||||
|
const targetLqi = nodeMap.get(e.target) ? getNodeLqi(nodeMap.get(e.target)!) : 255
|
||||||
|
const sourceLqi = nodeMap.get(e.source) ? getNodeLqi(nodeMap.get(e.source)!) : 255
|
||||||
|
const cost = 255 - Math.min(targetLqi, sourceLqi)
|
||||||
|
|
||||||
|
if (!adjacency.has(e.source)) adjacency.set(e.source, [])
|
||||||
|
if (!adjacency.has(e.target)) adjacency.set(e.target, [])
|
||||||
|
|
||||||
|
adjacency.get(e.source)!.push({ neighbor: e.target, edgeId: e.id, cost })
|
||||||
|
adjacency.get(e.target)!.push({ neighbor: e.source, edgeId: e.id, cost })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adjacency.has(startNodeId) || !adjacency.has(targetNodeId)) return []
|
||||||
|
|
||||||
|
const dist = new Map<string, number>()
|
||||||
|
const prev = new Map<string, { node: string; edgeId: string }>()
|
||||||
|
const unvisited = new Set(adjacency.keys())
|
||||||
|
|
||||||
|
for (const nodeId of unvisited) dist.set(nodeId, Infinity)
|
||||||
|
dist.set(startNodeId, 0)
|
||||||
|
|
||||||
|
while (unvisited.size > 0) {
|
||||||
|
let current: string | null = null
|
||||||
|
let minDist = Infinity
|
||||||
|
for (const nodeId of unvisited) {
|
||||||
|
const d = dist.get(nodeId)!
|
||||||
|
if (d < minDist) { minDist = d; current = nodeId }
|
||||||
|
}
|
||||||
|
if (current === null || minDist === Infinity) break
|
||||||
|
if (current === targetNodeId) break
|
||||||
|
|
||||||
|
unvisited.delete(current)
|
||||||
|
|
||||||
|
const neighbors = adjacency.get(current) ?? []
|
||||||
|
for (const { neighbor, edgeId, cost } of neighbors) {
|
||||||
|
if (!unvisited.has(neighbor)) continue
|
||||||
|
const alt = dist.get(current)! + cost
|
||||||
|
if (alt < (dist.get(neighbor) ?? Infinity)) {
|
||||||
|
dist.set(neighbor, alt)
|
||||||
|
prev.set(neighbor, { node: current, edgeId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct path: walk from targetNodeId back to startNodeId
|
||||||
|
const pathEdges: string[] = []
|
||||||
|
let current = targetNodeId
|
||||||
|
while (current !== startNodeId) {
|
||||||
|
const p = prev.get(current)
|
||||||
|
if (!p) return []
|
||||||
|
pathEdges.unshift(p.edgeId)
|
||||||
|
current = p.node
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathEdges
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user