diff --git a/.gitignore b/.gitignore index 0e18afd0..5849610d 100644 --- a/.gitignore +++ b/.gitignore @@ -319,7 +319,7 @@ pyrightconfig.json # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ [Bb]in [Ii]nclude -[Ll]ib +# [Ll]ib [Ll]ib64 [Ll]ocal [Ss]cripts diff --git a/backend_py/src/services/cameras/ehd.py b/backend_py/src/services/cameras/ehd.py index 73e3ec42..fde7b389 100644 --- a/backend_py/src/services/cameras/ehd.py +++ b/backend_py/src/services/cameras/ehd.py @@ -38,7 +38,7 @@ def _get_options(self) -> Dict[str, Option]: # Standard integer options options['bitrate'] = Option( self.cameras[2], '>I', xu.Unit.USR_ID, xu.Selector.USR_H264_CTRL, xu.Command.H264_BITRATE_CTRL, 'Bitrate', - lambda bitrate: int(bitrate * 1000000), # convert to bps from mpbs + lambda bitrate: int(round(bitrate * 1000000)), # convert to bps from mpbs (round for float imprecision) lambda bitrate: bitrate / 1000000 # convert to mpbs from bps ) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0f97dd8d..bc5d7e92 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "temp", "dependencies": { "@radix-ui/react-accordion": "^1.2.10", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.2.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.2", @@ -18,10 +19,11 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slider": "^1.3.3", - "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.3", "@radix-ui/react-tabs": "^1.1.9", "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.1.4", "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", @@ -34,6 +36,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "lucide-react": "^0.456.0", + "motion": "^12.23.26", "openapi-fetch": "^0.13.5", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -120,7 +123,6 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -1238,6 +1240,81 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", @@ -1388,6 +1465,24 @@ } } }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1419,22 +1514,22 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.11.tgz", - "integrity": "sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.7", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.6", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" @@ -1454,6 +1549,168 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", @@ -1646,6 +1903,24 @@ } } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", @@ -1749,6 +2024,24 @@ } } }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-radio-group": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.6.tgz", @@ -1984,6 +2277,24 @@ } } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz", @@ -2108,9 +2419,9 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -2259,6 +2570,78 @@ } } }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.4.tgz", @@ -2293,6 +2676,24 @@ } } }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2935,7 +3336,6 @@ "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2951,7 +3351,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2963,7 +3362,6 @@ "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3010,7 +3408,6 @@ "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/types": "8.31.0", @@ -3278,8 +3675,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/acorn": { "version": "8.14.1", @@ -3287,7 +3683,6 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3522,7 +3917,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -4016,7 +4410,6 @@ "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4356,6 +4749,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5589,6 +6009,47 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.26.tgz", + "integrity": "sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.23.26", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5948,7 +6409,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -6134,7 +6594,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6147,7 +6606,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6797,7 +7255,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -6949,7 +7406,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7230,7 +7686,6 @@ "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/package.json b/frontend/package.json index fbb24182..349e9d13 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.2.10", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.2.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.2", @@ -22,10 +23,11 @@ "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slider": "^1.3.3", - "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.3", "@radix-ui/react-tabs": "^1.1.9", "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-tooltip": "^1.1.4", "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", @@ -38,6 +40,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "lucide-react": "^0.456.0", + "motion": "^12.23.26", "openapi-fetch": "^0.13.5", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/public/single_wave.svg b/frontend/public/single_wave.svg new file mode 100644 index 00000000..4a931855 --- /dev/null +++ b/frontend/public/single_wave.svg @@ -0,0 +1,44 @@ + + + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 63e04e7e..4cef570e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,11 +24,32 @@ import { WifiDropdown } from "./components/dwe/wireless/wifi-dropdown"; import { WiredDropdown } from "./components/dwe/wireless/wired-dropdown"; import { SystemDropdown } from "./components/dwe/system/system-dropdown"; import { API_CLIENT } from "./api"; +import { TourAlertDialog, TourProvider, useTour } from "@/components/tour/tour"; +import { getSteps } from "./components/tour/tour-steps"; +import FeaturesContext from "./contexts/FeaturesContext"; +import { components } from "./schemas/dwe_os_2"; -function App() { - const socket = useRef(undefined); - const [connected, setConnected] = useState(false); - const [wifiAvailable, setWifiAvailable] = useState(false); +type WelcomeTourProps = { features: components["schemas"]["FeatureSupport"] }; +function WelcomeTourManager(props: WelcomeTourProps) { + const [openTour, setOpenTour] = useState(false); + const { setSteps } = useTour(); + + useEffect(() => { + setSteps(getSteps(props.features)); + const timer = setTimeout(() => { + setOpenTour(true); + }, 100); + + return () => clearTimeout(timer); + }, [setSteps, props.features]); + + return ; +} + +function AppContent() { + const [features, setFeatures] = useState< + components["schemas"]["FeatureSupport"] | undefined + >(undefined); const location = useLocation(); @@ -53,6 +74,55 @@ function App() { const pageTitle = getPageTitle(location.pathname); + useEffect(() => { + API_CLIENT.GET("/features").then((data) => { + if (data.data) setFeatures(data.data); + }); + }, []); + + return ( + + + +
+
+ +

DWE OS

+ + + + + + {pageTitle} + + + + + + +
+ + {features?.wifi ? : <>} + {features?.wifi ? : <>} + +
+
+
+
+ + + +
+
+ {features && } +
+ ); +} + +function App() { + const socket = useRef(undefined); + const [connected, setConnected] = useState(false); + const connectWebsocket = () => { if (socket.current) delete socket.current; @@ -80,48 +150,13 @@ function App() { } }, [connected]); - useEffect(() => { - API_CLIENT.GET("/features").then((data) => - data.data?.wifi ? setWifiAvailable(true) : setWifiAvailable(false) - ); - }, []); - return ( - - - -
-
- -

- DWE OS -

- - - - - - {pageTitle} - - - - - - - - - {wifiAvailable ? : <>} - -
-
-
- -
-
-
+ + +
); diff --git a/frontend/src/assets/animated-waves.tsx b/frontend/src/assets/animated-waves.tsx new file mode 100644 index 00000000..9898638b --- /dev/null +++ b/frontend/src/assets/animated-waves.tsx @@ -0,0 +1,63 @@ +import { cn } from "@/lib/utils"; + +const WaveSvg = ({ className }: { className?: string }) => ( + + + + +); + +export function AnimatedWaves({ className }: { className?: string }) { + return ( +
+ {/* bob animation */} + + + {/* top wave */} +
+ +
+ + {/* mid wave */} +
+ +
+ + {/* bot wave */} +
+ +
+
+ ); +} diff --git a/frontend/src/components/dwe/app/command-palette.tsx b/frontend/src/components/dwe/app/command-palette.tsx index f3c9e54c..4b5f87b7 100644 --- a/frontend/src/components/dwe/app/command-palette.tsx +++ b/frontend/src/components/dwe/app/command-palette.tsx @@ -7,8 +7,10 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; +import { Info } from "lucide-react"; export function CommandPalette() { const [open, setOpen] = useState(false); @@ -20,12 +22,12 @@ export function CommandPalette() { }; return ( - <> +
@@ -56,11 +58,13 @@ export function CommandPalette() { Cameras runCommand(() => navigate("/videos"))} + onSelect={() => runCommand(() => navigate("/recordings"))} > Recordings - runCommand(() => navigate("/log-viewer"))}> + runCommand(() => navigate("/log-viewer"))} + > Logs - +
); } diff --git a/frontend/src/components/dwe/cameras/camera-controls.tsx b/frontend/src/components/dwe/cameras/camera-controls.tsx index 6c2e8bf4..6f4ee6c0 100644 --- a/frontend/src/components/dwe/cameras/camera-controls.tsx +++ b/frontend/src/components/dwe/cameras/camera-controls.tsx @@ -13,8 +13,15 @@ import { DialogTrigger, // DialogFooter, // Optional: if you want a dedicated footer } from "@/components/ui/dialog"; -import { subscribe } from "valtio"; -import { RotateCcwIcon, SlidersHorizontal } from "lucide-react"; +import { subscribe, useSnapshot } from "valtio"; +import { + Aperture, + MonitorCog, + ImageIcon, + RotateCcwIcon, + SlidersHorizontal, + CircleEllipsis, +} from "lucide-react"; import IntegerControl from "./controls/integer-control"; import BooleanControl from "./controls/boolean-control"; @@ -29,6 +36,7 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; type ControlModel = components["schemas"]["ControlModel"]; @@ -63,6 +71,33 @@ const ControlWrapper = ({ }); }; + // handles disabling associated controls based on another (ie Auto Exposure turns off Exposure Time, Absolute) + // add any dependency / disabling pairings here + const dependencyName = control.name.includes("Exposure Time, Absolute") + ? "Auto Exposure" + : control.name.includes("White Balance Temperature") + ? "White Balance, Auto" + : control.name.includes("Bitrate") + ? "Variable Bitrate" + : null; + + const dependencyControl = dependencyName + ? device.controls.find((c) => c.name.includes(dependencyName)) + : null; + + const dependencySnap = dependencyControl + ? useSnapshot(dependencyControl) + : null; + + let isDisabled = false; + if (dependencySnap) { + if (control.name.includes("Exposure Time, Absolute")) { + isDisabled = dependencySnap.value !== 1; + } else { + isDisabled = !!dependencySnap.value; + } + } + useEffect(() => { const unsub = subscribe(control, () => { setUVCControl(bus_info, control.value, control.control_id); @@ -79,7 +114,9 @@ const ControlWrapper = ({ switch (control.flags.control_type) { case "INTEGER": - return ; + return ( + + ); case "BOOLEAN": return ; case "MENU": @@ -118,6 +155,17 @@ export const CameraControls = () => { return "Miscellaneous"; }; + const getTypeRank = (a: ControlModel, b: ControlModel, order: string[]) => { + const typeRankA = order.indexOf(a.flags.control_type); + const typeRankB = order.indexOf(b.flags.control_type); + + if (typeRankA !== typeRankB) { + return typeRankA - typeRankB; + } + + return a.name.localeCompare(b.name); + }; + const groupedControls = supportedControls.reduce( (acc, control) => { const groupName = getGroupName(control.name); @@ -131,24 +179,41 @@ export const CameraControls = () => { }, {} ); + const typeOrder = ["INTEGER", "MENU"]; - const order = [ + const groupOrder = [ "Exposure Controls", "Image Controls", "System Controls", "Miscellaneous", ]; + const groupIcons: { [key: string]: React.ReactNode } = { + "Exposure Controls": , + "Image Controls": , + "System Controls": , + // misc / additional icon is handled in the Accordion + }; + return ( - - + - Camera Controls + +
+ + Camera Controls +
+
Adjust settings for the selected camera. Changes are applied immediately. @@ -158,27 +223,57 @@ export const CameraControls = () => { {supportedControls.length > 0 ? (
{Object.keys(groupedControls) - .sort((a, b) => order.indexOf(a) - order.indexOf(b)) - .map((groupName) => ( - - - - {groupName} - - -
- {groupedControls[groupName].map((control, index) => ( - - ))} -
-
-
-
- ))} + .sort((a, b) => groupOrder.indexOf(a) - groupOrder.indexOf(b)) + .map((groupName) => { + const booleans = groupedControls[groupName].filter( + (c) => c.flags.control_type === "BOOLEAN" + ); + const others = groupedControls[groupName].filter( + (c) => c.flags.control_type !== "BOOLEAN" + ); + return ( + + + +
+ {groupIcons[groupName] || ( + + )} + {groupName} +
+
+ + {others && ( +
+ {others + .sort((a, b) => { + return getTypeRank(a, b, typeOrder); + }) + .map((control, index) => ( + + ))} +
+ )} + {booleans && ( +
+ {booleans.map((control, index) => ( + + ))} +
+ )} +
+
+
+ ); + })}
) : (

diff --git a/frontend/src/components/dwe/cameras/controls/boolean-control.tsx b/frontend/src/components/dwe/cameras/controls/boolean-control.tsx index aeb4afdf..aa1ced8b 100644 --- a/frontend/src/components/dwe/cameras/controls/boolean-control.tsx +++ b/frontend/src/components/dwe/cameras/controls/boolean-control.tsx @@ -1,4 +1,4 @@ -import { Switch } from "@/components/ui/switch"; +import { Toggle } from "@/components/ui/toggle"; import { components } from "@/schemas/dwe_os_2"; import { useState } from "react"; import { subscribe } from "valtio"; @@ -32,8 +32,10 @@ const BooleanControl = ({ return (

- {control.name} - + + +
{control.name}
+
); }; diff --git a/frontend/src/components/dwe/cameras/controls/integer-control.tsx b/frontend/src/components/dwe/cameras/controls/integer-control.tsx index d772b654..e27b1780 100644 --- a/frontend/src/components/dwe/cameras/controls/integer-control.tsx +++ b/frontend/src/components/dwe/cameras/controls/integer-control.tsx @@ -5,29 +5,43 @@ import { Input } from "@/components/ui/input"; import { components } from "@/schemas/dwe_os_2"; import { useState, useEffect, useCallback, useRef } from "react"; import { subscribe } from "valtio"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { CirclePlus, CircleMinus } from "lucide-react"; const IntegerControl = ({ control, + isDisabled = false, }: { control: components["schemas"]["ControlModel"]; + isDisabled?: boolean; }) => { + const { min_value, max_value, step } = control.flags; + const controlId = `control-${control.control_id}-${control.name}`; + const safeStep = step && step > 0 ? step : 1; + + const precision = + safeStep < 1 ? step.toString().split(".")[1]?.length || 0 : 0; + const [currentValue, setCurrentValue] = useState(control.value); - const [inputValue, setInputValue] = useState(control.value.toString()); + const [inputValue, setInputValue] = useState( + control.value.toFixed(precision).toString() + ); const containerRef = useRef(null); + const inputRef = useRef(null); + const dragState = useRef<{ startX: number; startValue: number; containerWidth: number; } | null>(null); - const { min_value, max_value, step } = control.flags; - const controlId = `control-${control.control_id}-${control.name}`; useEffect(() => { const unsubscribe = subscribe(control, () => { if (control.value !== currentValue) { setCurrentValue(control.value); - setInputValue(control.value.toString()); + setInputValue(control.value.toFixed(precision).toString()); } }); return () => unsubscribe(); @@ -41,7 +55,7 @@ const IntegerControl = ({ const snapToStep = useCallback( (val: number): number => { if (!step || step <= 0) return val; - return Math.round((val - min_value) / step) * step + min_value; + return Math.round((val - min_value) / safeStep) * safeStep + min_value; }, [min_value, step] ); @@ -54,7 +68,7 @@ const IntegerControl = ({ validatedValue = snapToStep(validatedValue); setCurrentValue(validatedValue); - setInputValue(validatedValue.toString()); + setInputValue(validatedValue.toFixed(precision).toString()); if (control.value !== validatedValue) { control.value = validatedValue; @@ -90,7 +104,7 @@ const IntegerControl = ({ let newValue = clamp(startValue + valueDelta); setCurrentValue(newValue); - setInputValue(Math.round(newValue).toString()); + setInputValue(newValue.toFixed(precision).toString()); }; const handlePointerUp = (e: React.PointerEvent) => { @@ -107,7 +121,7 @@ const IntegerControl = ({ }; const handleInputBlur = () => { - const parsedValue = parseInt(inputValue, 10); + const parsedValue = parseFloat(inputValue); if (!isNaN(parsedValue)) { commitValue(parsedValue); } else { @@ -117,7 +131,7 @@ const IntegerControl = ({ const handleInputKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Enter") { - const parsedValue = parseInt(inputValue, 10); + const parsedValue = parseFloat(inputValue); if (!isNaN(parsedValue)) { commitValue(parsedValue); event.currentTarget.blur(); @@ -130,6 +144,26 @@ const IntegerControl = ({ } }; + const handleInputStep = (step: string) => { + if (isDisabled || !inputRef.current) return; + + try { + if (step === "up") { + inputRef.current.stepUp(); + } else { + inputRef.current.stepDown(); + } + } catch (e) { + return; + } + + // stepUp and stepDown don't call on change like the arrow keys do, so we need to update react from the dom + const newValue = inputRef.current.value; + setInputValue(newValue); + setCurrentValue(parseFloat(newValue)); + + inputRef.current.focus(); + }; // // Handle slider live updates // const handleSliderChange = (value: number[]) => { // // We allow the "raw" value (step 1) to flow through here for smooth UI @@ -145,9 +179,14 @@ const IntegerControl = ({ // }; return ( -
+
-
+
{control.name}
- (e.target as HTMLInputElement).blur()} - /> +
+ (e.target as HTMLInputElement).blur()} + disabled={isDisabled} + ref={inputRef} + /> +
+ + +
+
); diff --git a/frontend/src/components/dwe/cameras/device-list.tsx b/frontend/src/components/dwe/cameras/device-list.tsx index b5c2c321..f18f0f72 100644 --- a/frontend/src/components/dwe/cameras/device-list.tsx +++ b/frontend/src/components/dwe/cameras/device-list.tsx @@ -17,9 +17,57 @@ import DevicesContext from "@/contexts/DevicesContext"; import { getDeviceByBusInfo } from "@/lib/utils"; import NotConnected from "../not-connected"; import { useToast } from "@/hooks/use-toast"; +import { useTour } from "@/components/tour/tour"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; type DeviceModel = components["schemas"]["DeviceModel"]; +const DEMO_DEVICE: DeviceModel = { + bus_info: "demo-device", + device_type: 0, + nickname: "Demo Camera", + manufacturer: "DeepWater Exploration", + name: "exploreHD", + + vid: 1234, + pid: 5678, + + is_managed: false, + followers: [], + device_info: { + device_name: "exploreHD Demo", + bus_info: "demo-device", + device_paths: ["/dev/video99"], + vid: 1234, + pid: 5678, + }, + controls: [], + stream: { + device_path: "/dev/video99", + encode_type: "H264", + stream_type: "UDP", + endpoints: [{ host: "192.168.1.100", port: 5600 }], + width: 1920, + height: 1080, + interval: { numerator: 1, denominator: 30 }, + enabled: true, + }, + cameras: [ + { + path: "/dev/video99", + formats: { + H264: [ + { + width: 1920, + height: 1080, + intervals: [{ numerator: 1, denominator: 30 }], + }, + ], + }, + }, + ], +}; + const NoDevicesConnected = () => { return (
@@ -55,6 +103,8 @@ const DeviceListLayout = () => { const { toast } = useToast(); + const { isActive } = useTour(); + const [devices, setDevices] = useState([] as DeviceModel[]); const [savedPreferences, setSavedPreferences] = useState({ @@ -62,6 +112,7 @@ const DeviceListLayout = () => { } as components["schemas"]["SavedPreferencesModel"]); const [nextPort, setNextPort] = useState(5600); + const [demoDeviceProxy] = useState(() => proxy(DEMO_DEVICE)); const getNextPort = (devs: DeviceModel[]) => { const allPorts = devs.flatMap((device) => @@ -186,28 +237,40 @@ const DeviceListLayout = () => { device.stream.enabled = true; }; + const displayDevices = + isActive && devices.length === 0 ? [demoDeviceProxy] : devices; + return ( -
- d.device_type == 2), - enableStream, - }} - > - {devices.map((device, index) => ( -
- - - -
- ))} - {devices.length === 0 && - (connected ? : )} -
+
+
+ d.device_type == 2), + enableStream, + }} + > + {displayDevices.map((device, index) => ( +
+ + + +
+ ))} + {displayDevices.length === 0 && + (connected ? : )} +
+
); }; diff --git a/frontend/src/components/dwe/cameras/nickname.tsx b/frontend/src/components/dwe/cameras/nickname.tsx index 371fc466..3f80700f 100644 --- a/frontend/src/components/dwe/cameras/nickname.tsx +++ b/frontend/src/components/dwe/cameras/nickname.tsx @@ -5,6 +5,7 @@ import { Check, Edit2, X } from "lucide-react"; import { useSnapshot } from "valtio"; import DeviceContext from "@/contexts/DeviceContext"; import { API_CLIENT } from "@/api"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; export const CameraNickname = () => { const device = useContext(DeviceContext)!; @@ -54,7 +55,7 @@ export const CameraNickname = () => { }; return ( -
+
-
+
@@ -223,6 +223,7 @@ const EndpointList = ({ {/* Add Button */}
- - ) : ( - - )} + +
); }; diff --git a/frontend/src/components/dwe/recordings/recordings.tsx b/frontend/src/components/dwe/recordings/recordings.tsx index 60806760..a14af884 100644 --- a/frontend/src/components/dwe/recordings/recordings.tsx +++ b/frontend/src/components/dwe/recordings/recordings.tsx @@ -1,12 +1,43 @@ import { API_CLIENT } from "@/api"; import { Button } from "@/components/ui/button"; -import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { components } from "@/schemas/dwe_os_2"; -import { Separator } from "@radix-ui/react-separator"; -import { useEffect, useLayoutEffect, useRef, useState } from "react"; -import { Download, Trash } from "lucide-react"; +import { Separator } from "@/components/ui/separator"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { + Download, + FolderArchive, + Trash, + Video, + VideoOff, + X, +} from "lucide-react"; +import { useTour } from "@/components/tour/tour"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, + TruncatedTooltip, +} from "@/components/ui/tooltip"; + type RecordingInfo = components["schemas"]["RecordingInfo"]; +const DEMO_RECORDING: RecordingInfo = { + path: "", + name: "Demo Recording", + format: "mp4", + duration: "00:00:00", + size: "0", + created: new Date().toISOString(), +}; + const formatFileSize = (sizeInMB: number): string => { if (sizeInMB >= 1024 * 1024) { return `${(sizeInMB / (1024 * 1024)).toFixed(2)} TB`; @@ -34,6 +65,8 @@ const Recordings = () => { null ); + const { isActive } = useTour(); + const sortRecordings = () => { var modifier = (x: any) => x; if (sortColumn === "size") { @@ -138,6 +171,7 @@ const Recordings = () => { setShowMenu(true); setRightClickedRecording(selected); }; + useEffect(() => { // Fetch recordings data from the backend API_CLIENT.GET("/recordings") @@ -148,14 +182,37 @@ const Recordings = () => { .catch((error) => console.error("Error fetching recordings:", error)) .finally(() => setLoading(false)); }, []); + + const displayRecordings = useMemo(() => { + let data = isActive ? [DEMO_RECORDING] : recordings; + if (!sortColumn || !sortDirection) return data; + + return [...data].sort((a, b) => { + let valA: any = a[sortColumn]; + let valB: any = b[sortColumn]; + + if (sortColumn === "size") { + valA = parseFloat(valA || "0"); + valB = parseFloat(valB || "0"); + } + + if (valA < valB) return sortDirection === "asc" ? -1 : 1; + if (valA > valB) return sortDirection === "asc" ? 1 : -1; + return 0; + }); + }, [recordings, sortColumn, sortDirection, isActive]); + return ( -
+
{/* handles right click on recordings */} {showMenu && (

@@ -194,7 +251,7 @@ const Recordings = () => {

{ if (rightClickedRecording) { // @ts-ignore-next-line @@ -218,186 +275,239 @@ const Recordings = () => {
)} {/* handles recording display */} - {loading ? ( -
- Loading... -
- ) : ( - - - handleSort("name")} - > - Name   - {sortColumn === "name" && (sortDirection === "asc" ? "â–²" : "â–¼")} - - handleSort("created")} - > - Created   - {sortColumn === "created" && - (sortDirection === "asc" ? "â–²" : "â–¼")} - - handleSort("duration")} - > - Duration   - {sortColumn === "duration" && - (sortDirection === "asc" ? "â–²" : "â–¼")} - - handleSort("size")} - > - Size   - {sortColumn === "size" && (sortDirection === "asc" ? "â–²" : "â–¼")} - - - - {recordings.map((recording) => ( - { - if (selectedRecording === recording) { - setSelectedRecording(null); - } else { - setSelectedRecording(recording); - } - }} - onContextMenu={(e) => handleContextMenu(recording, e)} - className={ - selectedRecording === recording - ? "bg-muted cursor-pointer" - : "cursor-pointer bg-background hover:bg-accent rounded-xl" - } - > - - {recording.name}.{recording.format} +
+ {" "} + {loading ? ( +
+ Loading... +
+ ) : ( +
+ + + handleSort("name")} + > + Name   + {sortColumn === "name" && + (sortDirection === "asc" ? "â–²" : "â–¼")} - - {recording.created} + handleSort("created")} + > + Created   + {sortColumn === "created" && + (sortDirection === "asc" ? "â–²" : "â–¼")} - - {recording.duration} + handleSort("duration")} + > + Duration   + {sortColumn === "duration" && + (sortDirection === "asc" ? "â–²" : "â–¼")} - - {formatFileSize( - recording.size ? parseFloat(recording.size) : 0 - )} + handleSort("size")} + > + Size   + {sortColumn === "size" && + (sortDirection === "asc" ? "â–²" : "â–¼")} - ))} - -
- )} + + + + {displayRecordings.map((recording) => ( + { + if (selectedRecording === recording) { + setSelectedRecording(null); + } else { + setSelectedRecording(recording); + } + }} + onContextMenu={(e) => handleContextMenu(recording, e)} + className={ + selectedRecording === recording + ? "bg-accent cursor-pointer" + : "cursor-pointer bg-background hover:bg-muted rounded-xl" + } + > + +
+ + + + {recording?.format === "mp4" ? ( + + + +

+ {recording?.format === "mp4" + ? "Playable in browser" + : "Download required to play"} +

+
+
+ +
+
+ + {recording.created} + + + {recording.duration} + + + {formatFileSize( + recording.size ? parseFloat(recording.size) : 0 + )} + +
+ ))} +
+ + )} +
{/* handles recording detailed view */}
-

- Name -
-

- {selectedRecording?.name}.{selectedRecording?.format} -

-

- {selectedRecording?.format === "mp4" ? ( - - ) : ( -
-

- .{selectedRecording?.format} is not supported in the browser - video player. -
- Use the download button to download the file and play it in a - compatible player like VLC. -

-
- )} -
-
-

- Format:{" "} - {selectedRecording?.format - .toLocaleUpperCase() - .replace("MP4", "MPEG-4")} -

-

- Created:{" "} - {selectedRecording?.created} -

+
setSelectedRecording(null)} + className="flex flex-col justify-center my-8 h-auto cursor-pointer group hover:bg-accent bg-sidebar/50 backdrop-blur border border-r-0 rounded-l-2xl" + > + +
+
+ {selectedRecording?.format === "mp4" ? ( + + ) : ( +
+

+ .{selectedRecording?.format} is not supported in the browser + video player. +
+ Use the download button to download the file and play it in a + compatible player like VLC. +

+
+ )} +
+

+ {selectedRecording?.name}.{selectedRecording?.format} +

-
-

- Duration:{" "} - {selectedRecording?.duration} -

-

- Size:{" "} - {formatFileSize( - selectedRecording?.size - ? parseFloat(selectedRecording.size) - : 0 - )} -

+
+ +
+

+ + Format: + {" "} + {selectedRecording?.format + .toLocaleUpperCase() + .replace("MP4", "MPEG-4")} +

+

+ + Created: + {" "} + {selectedRecording?.created} +

+

+ + Duration: + {" "} + {selectedRecording?.duration} +

+

+ + Size: + {" "} + {formatFileSize( + selectedRecording?.size + ? parseFloat(selectedRecording.size) + : 0 + )} +

+
-
-
- - + + Download + + + +
- - {/* Sticky footer at bottom of viewport */} -
+ {/* footer at bottom of viewport */} +
-
Total Recordings: {recordings.length} diff --git a/frontend/src/components/dwe/system/system-dropdown.tsx b/frontend/src/components/dwe/system/system-dropdown.tsx index c07c9959..b37a364f 100644 --- a/frontend/src/components/dwe/system/system-dropdown.tsx +++ b/frontend/src/components/dwe/system/system-dropdown.tsx @@ -18,6 +18,7 @@ import { import { useToast } from "@/hooks/use-toast"; import { API_CLIENT } from "@/api"; import { useState } from "react"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; export function SystemDropdown() { const { toast } = useToast(); @@ -46,11 +47,14 @@ export function SystemDropdown() { const confirmAction = (type: "restart" | "shutdown") => { setAction(type); - setDialogOpen(true); + // radix dialog / dropdown race condition fix + setTimeout(() => { + setDialogOpen(true); + }, 100); }; return ( - <> +
- + ); } diff --git a/frontend/src/components/dwe/terminal/terminal.tsx b/frontend/src/components/dwe/terminal/terminal.tsx index c1157c03..16d776ec 100644 --- a/frontend/src/components/dwe/terminal/terminal.tsx +++ b/frontend/src/components/dwe/terminal/terminal.tsx @@ -5,6 +5,7 @@ import { Xterm, ClientOptions } from "./xterm"; import { ITerminalOptions } from "@xterm/xterm"; import WebsocketContext from "@/contexts/WebsocketContext"; import { TTYD_TOKEN_URL, TTYD_WS } from "@/api"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; const darkTermColors = { background: "#1d1e23", @@ -29,7 +30,7 @@ const darkTermColors = { }; const lightTermColors = { - background: "#e0e2ee", + background: "#ffffff", foreground: "#141522", cursor: "#525476", black: "#d2d3e6", @@ -141,7 +142,11 @@ export const Terminal = () => { >
-
+
diff --git a/frontend/src/components/dwe/wireless/wifi-dropdown.tsx b/frontend/src/components/dwe/wireless/wifi-dropdown.tsx index 9bbb2bcd..aa470935 100644 --- a/frontend/src/components/dwe/wireless/wifi-dropdown.tsx +++ b/frontend/src/components/dwe/wireless/wifi-dropdown.tsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from "react"; -import { Wifi, WifiOff, Check, Lock } from "lucide-react"; +import { Wifi, WifiOff, Dot, Lock } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, @@ -24,6 +24,8 @@ import { import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { useToast } from "@/hooks/use-toast"; +import { Switch } from "@/components/ui/switch"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; export function WifiDropdown() { const { toast } = useToast(); @@ -72,7 +74,7 @@ export function WifiDropdown() { }; } - return () => { }; + return () => {}; }, [connected]); const toggleWifi = async () => { @@ -123,8 +125,6 @@ export function WifiDropdown() { setPasswordDialogOpen(false); }; - - const handlePasswordSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!selectedNetwork) return; @@ -135,7 +135,7 @@ export function WifiDropdown() { }; return ( - <> +
+ toggleWifi()} + /> @@ -196,7 +192,7 @@ export function WifiDropdown() { )}
{wifiStatus?.connection?.id == network.ssid && ( - +
)} ))} @@ -256,7 +252,7 @@ export function WifiDropdown() { - +
); } @@ -267,7 +263,6 @@ interface SignalStrengthProps { function SignalStrength({ strength }: SignalStrengthProps) { const thresholds = [20, 50, 70]; - return (
{thresholds.map((threshold, index) => ( diff --git a/frontend/src/components/dwe/wireless/wired-dropdown.tsx b/frontend/src/components/dwe/wireless/wired-dropdown.tsx index a40c85f1..9a7b9548 100644 --- a/frontend/src/components/dwe/wireless/wired-dropdown.tsx +++ b/frontend/src/components/dwe/wireless/wired-dropdown.tsx @@ -11,12 +11,13 @@ import { API_CLIENT } from "@/api"; import { components } from "@/schemas/dwe_os_2"; // Assuming your schema is at this path import WebsocketContext from "@/contexts/WebsocketContext"; import { useToast } from "@/hooks/use-toast"; -import { useContext, useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; // Import Shadcn UI components for form elements import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; // Import types from the generated schema type IPConfiguration = components["schemas"]["IPConfiguration"]; @@ -207,7 +208,7 @@ export function WiredDropdown() { }, [ipConfiguration]); return ( - <> +
); } diff --git a/frontend/src/components/themes/mode-toggle.tsx b/frontend/src/components/themes/mode-toggle.tsx index f8f48865..355636eb 100644 --- a/frontend/src/components/themes/mode-toggle.tsx +++ b/frontend/src/components/themes/mode-toggle.tsx @@ -3,6 +3,7 @@ import { Moon, Sun, SunMoon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useTheme } from "@/components/themes/theme-provider"; +import { TOUR_STEP_IDS } from "@/lib/tour-constants"; export function ModeToggle() { const { setTheme, theme } = useTheme(); @@ -15,7 +16,12 @@ export function ModeToggle() { }; return ( - + + {steps[currentStep]?.content} + +
+
+ {currentStep + 1} / {steps.length} +
+ {currentStep > 0 && ( + <> + + + + )} + +
+
+ + + + )} + + + ); +} + +export function useTour() { + const context = useContext(TourContext); + if (!context) { + throw new Error("useTour must be used within a TourProvider"); + } + return context; +} + +export function TourAlertDialog({ + isOpen, + setIsOpen, +}: { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +}) { + const { startTour, steps, isTourCompleted, setIsTourCompleted, currentStep } = + useTour(); + + if (isTourCompleted || steps.length === 0 || currentStep > -1) { + return null; + } + const handleSkip = async () => { + setIsOpen(false); + setIsTourCompleted(true); + }; + + return ( + + + +
+ +
+ + Welcome to DWE OS + + + Take a quick tour to learn about the key features and functionality + of DWE OS. +
+
+
+ You can restart this tour anytime in{" "} + Preferences +
+
+
+
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/ui/accordion.tsx b/frontend/src/components/ui/accordion.tsx index 301a5608..13257f22 100644 --- a/frontend/src/components/ui/accordion.tsx +++ b/frontend/src/components/ui/accordion.tsx @@ -13,7 +13,7 @@ const AccordionItem = React.forwardRef< , React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - + {children} - + )); diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..6cd72f4a --- /dev/null +++ b/frontend/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 45d50a56..9f066749 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + "cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { @@ -17,7 +17,7 @@ const buttonVariants = cva( "border bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-background", + ghost: "hover:bg-accent/20", link: "text-primary underline-offset-4 hover:underline", svg: "hover:text-accent", }, @@ -42,12 +42,13 @@ export interface ButtonProps } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ({ className, variant, size, asChild = false, id, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ( ); diff --git a/frontend/src/components/ui/command.tsx b/frontend/src/components/ui/command.tsx index 0db642a6..340f3808 100644 --- a/frontend/src/components/ui/command.tsx +++ b/frontend/src/components/ui/command.tsx @@ -1,10 +1,10 @@ -import * as React from "react" -import { type DialogProps } from "@radix-ui/react-dialog" -import { Command as CommandPrimitive } from "cmdk" -import { Search } from "lucide-react" +import * as React from "react"; +import { type DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; -import { cn } from "@/lib/utils" -import { Dialog, DialogContent } from "@/components/ui/dialog" +import { cn } from "@/lib/utils"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; const Command = React.forwardRef< React.ElementRef, @@ -13,13 +13,13 @@ const Command = React.forwardRef< -)) -Command.displayName = CommandPrimitive.displayName +)); +Command.displayName = CommandPrimitive.displayName; const CommandDialog = ({ children, ...props }: DialogProps) => { return ( @@ -30,8 +30,8 @@ const CommandDialog = ({ children, ...props }: DialogProps) => { - ) -} + ); +}; const CommandInput = React.forwardRef< React.ElementRef, @@ -48,9 +48,9 @@ const CommandInput = React.forwardRef< {...props} />
-)) +)); -CommandInput.displayName = CommandPrimitive.Input.displayName +CommandInput.displayName = CommandPrimitive.Input.displayName; const CommandList = React.forwardRef< React.ElementRef, @@ -61,9 +61,9 @@ const CommandList = React.forwardRef< className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} {...props} /> -)) +)); -CommandList.displayName = CommandPrimitive.List.displayName +CommandList.displayName = CommandPrimitive.List.displayName; const CommandEmpty = React.forwardRef< React.ElementRef, @@ -74,9 +74,9 @@ const CommandEmpty = React.forwardRef< className="py-6 text-center text-sm" {...props} /> -)) +)); -CommandEmpty.displayName = CommandPrimitive.Empty.displayName +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; const CommandGroup = React.forwardRef< React.ElementRef, @@ -90,9 +90,9 @@ const CommandGroup = React.forwardRef< )} {...props} /> -)) +)); -CommandGroup.displayName = CommandPrimitive.Group.displayName +CommandGroup.displayName = CommandPrimitive.Group.displayName; const CommandSeparator = React.forwardRef< React.ElementRef, @@ -103,8 +103,8 @@ const CommandSeparator = React.forwardRef< className={cn("-mx-1 h-px bg-border", className)} {...props} /> -)) -CommandSeparator.displayName = CommandPrimitive.Separator.displayName +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; const CommandItem = React.forwardRef< React.ElementRef, @@ -113,14 +113,14 @@ const CommandItem = React.forwardRef< -)) +)); -CommandItem.displayName = CommandPrimitive.Item.displayName +CommandItem.displayName = CommandPrimitive.Item.displayName; const CommandShortcut = ({ className, @@ -134,9 +134,9 @@ const CommandShortcut = ({ )} {...props} /> - ) -} -CommandShortcut.displayName = "CommandShortcut" + ); +}; +CommandShortcut.displayName = "CommandShortcut"; export { Command, @@ -148,4 +148,4 @@ export { CommandItem, CommandShortcut, CommandSeparator, -} +}; diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index da3aedbc..1aeec614 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -38,7 +38,7 @@ const DialogContent = React.forwardRef< , React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, children, ...props }, ref) => ( -)) +)); DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName + DropdownMenuPrimitive.SubTrigger.displayName; const DropdownMenuSubContent = React.forwardRef< React.ElementRef, @@ -50,9 +50,9 @@ const DropdownMenuSubContent = React.forwardRef< )} {...props} /> -)) +)); DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName + DropdownMenuPrimitive.SubContent.displayName; const DropdownMenuContent = React.forwardRef< React.ElementRef, @@ -63,20 +63,20 @@ const DropdownMenuContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", + "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover/30 backdrop-blur p-1 text-popover-foreground shadow-md", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className )} {...props} /> -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; const DropdownMenuItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; const DropdownMenuCheckboxItem = React.forwardRef< React.ElementRef, @@ -111,9 +111,9 @@ const DropdownMenuCheckboxItem = React.forwardRef< {children} -)) +)); DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName + DropdownMenuPrimitive.CheckboxItem.displayName; const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, @@ -134,13 +134,13 @@ const DropdownMenuRadioItem = React.forwardRef< {children} -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; const DropdownMenuLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { - inset?: boolean + inset?: boolean; } >(({ className, inset, ...props }, ref) => ( -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; const DropdownMenuSeparator = React.forwardRef< React.ElementRef, @@ -161,11 +161,11 @@ const DropdownMenuSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ className, @@ -176,9 +176,9 @@ const DropdownMenuShortcut = ({ className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} /> - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; export { DropdownMenu, @@ -196,4 +196,4 @@ export { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup, -} +}; diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 89800e2b..e4e43a98 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>( - +
); diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx index 20d36bdb..3e68d5cb 100644 --- a/frontend/src/components/ui/select.tsx +++ b/frontend/src/components/ui/select.tsx @@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between whitespace-nowrap hover:bg-accent rounded-md border border-muted bg-background/30 px-3 py-2 text-sm shadow-md ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className )} {...props} @@ -73,7 +73,7 @@ const SelectContent = React.forwardRef<