From c3f9514182bcba9042808d0b2fce8f2eecf76a25 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Wed, 6 May 2026 10:47:40 +0200 Subject: [PATCH] wip --- client/ui-wails/.gitignore | 1 + client/ui-wails/frontend/index.html | 2 +- client/ui-wails/frontend/package.json | 11 + client/ui-wails/frontend/pnpm-lock.yaml | 829 +++++++++++++++++- client/ui-wails/frontend/src/App.tsx | 35 - client/ui-wails/frontend/src/app.tsx | 39 + .../frontend/src/components/Button.tsx | 183 +++- .../frontend/src/components/Dialog.tsx | 149 ++++ .../frontend/src/components/HelpText.tsx | 22 + .../frontend/src/components/IconButton.tsx | 41 + .../frontend/src/components/Input.tsx | 197 ++++- .../frontend/src/components/Label.tsx | 40 + .../frontend/src/components/NavItem.tsx | 81 ++ .../src/components/NetBirdConnectToggle.tsx | 2 +- .../src/components/NewProfileDialog.tsx | 73 ++ .../src/components/PlaceholderHeader.tsx | 2 +- .../src/components/ProfileSelector.tsx | 407 +++++++++ .../frontend/src/components/SearchInput.tsx | 30 + client/ui-wails/frontend/src/globals.css | 1 + .../frontend/src/{Layout.tsx => layout.tsx} | 0 .../frontend/src/layouts/ConnectionStatus.tsx | 25 + .../ui-wails/frontend/src/layouts/Header.tsx | 12 + .../frontend/src/layouts/Navigation.tsx | 31 + client/ui-wails/frontend/src/lib/color.ts | 17 + .../src/modules/peers/PeerFilters.tsx | 53 ++ .../frontend/src/modules/peers/PeersList.tsx | 51 ++ .../src/modules/peers/PeersModule.tsx | 73 ++ .../frontend/src/modules/peers/mockPeers.ts | 385 ++++++++ .../frontend/src/modules/peers/types.ts | 20 + client/ui-wails/frontend/src/screens/Main.tsx | 76 +- client/ui-wails/frontend/tailwind.config.ts | 96 ++ client/ui-wails/main.go | 6 +- 32 files changed, 2815 insertions(+), 175 deletions(-) delete mode 100644 client/ui-wails/frontend/src/App.tsx create mode 100644 client/ui-wails/frontend/src/app.tsx create mode 100644 client/ui-wails/frontend/src/components/Dialog.tsx create mode 100644 client/ui-wails/frontend/src/components/HelpText.tsx create mode 100644 client/ui-wails/frontend/src/components/IconButton.tsx create mode 100644 client/ui-wails/frontend/src/components/Label.tsx create mode 100644 client/ui-wails/frontend/src/components/NavItem.tsx create mode 100644 client/ui-wails/frontend/src/components/NewProfileDialog.tsx create mode 100644 client/ui-wails/frontend/src/components/ProfileSelector.tsx create mode 100644 client/ui-wails/frontend/src/components/SearchInput.tsx rename client/ui-wails/frontend/src/{Layout.tsx => layout.tsx} (100%) create mode 100644 client/ui-wails/frontend/src/layouts/ConnectionStatus.tsx create mode 100644 client/ui-wails/frontend/src/layouts/Header.tsx create mode 100644 client/ui-wails/frontend/src/layouts/Navigation.tsx create mode 100644 client/ui-wails/frontend/src/lib/color.ts create mode 100644 client/ui-wails/frontend/src/modules/peers/PeerFilters.tsx create mode 100644 client/ui-wails/frontend/src/modules/peers/PeersList.tsx create mode 100644 client/ui-wails/frontend/src/modules/peers/PeersModule.tsx create mode 100644 client/ui-wails/frontend/src/modules/peers/mockPeers.ts create mode 100644 client/ui-wails/frontend/src/modules/peers/types.ts diff --git a/client/ui-wails/.gitignore b/client/ui-wails/.gitignore index d779b3d07..9f233d8b6 100644 --- a/client/ui-wails/.gitignore +++ b/client/ui-wails/.gitignore @@ -3,5 +3,6 @@ bin frontend/dist frontend/node_modules frontend/bindings +frontend/.vite build/linux/appimage/build build/windows/nsis/MicrosoftEdgeWebview2Setup.exe diff --git a/client/ui-wails/frontend/index.html b/client/ui-wails/frontend/index.html index f540ff29c..93aa1039b 100644 --- a/client/ui-wails/frontend/index.html +++ b/client/ui-wails/frontend/index.html @@ -1,5 +1,5 @@ - + diff --git a/client/ui-wails/frontend/package.json b/client/ui-wails/frontend/package.json index c9f452459..18c8616b2 100644 --- a/client/ui-wails/frontend/package.json +++ b/client/ui-wails/frontend/package.json @@ -11,17 +11,28 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-visually-hidden": "^1.2.4", "@wailsio/runtime": "latest", + "chroma-js": "^3.2.0", "class-variance-authority": "^0.7.1", + "classnames": "^2.5.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "framer-motion": "^12.38.0", "lucide-react": "^0.469.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-loading-skeleton": "^3.5.0", "react-router-dom": "^7.1.3", "tailwind-merge": "^2.6.0" }, "devDependencies": { + "@types/chroma-js": "^3.1.2", "@types/node": "^25.6.0", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/client/ui-wails/frontend/pnpm-lock.yaml b/client/ui-wails/frontend/pnpm-lock.yaml index 7a3469361..05f06e597 100644 --- a/client/ui-wails/frontend/pnpm-lock.yaml +++ b/client/ui-wails/frontend/pnpm-lock.yaml @@ -5,15 +5,42 @@ settings: excludeLinksFromLockfile: false dependencies: + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.8(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: ^1.2.10 + version: 1.2.10(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-visually-hidden': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) '@wailsio/runtime': specifier: latest version: 3.0.0-alpha.79 + chroma-js: + specifier: ^3.2.0 + version: 3.2.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 + classnames: + specifier: ^2.5.1 + version: 2.5.1 clsx: specifier: ^2.1.1 version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) framer-motion: specifier: ^12.38.0 version: 12.38.0(react-dom@18.3.1)(react@18.3.1) @@ -26,6 +53,9 @@ dependencies: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-loading-skeleton: + specifier: ^3.5.0 + version: 3.5.0(react@18.3.1) react-router-dom: specifier: ^7.1.3 version: 7.14.2(react-dom@18.3.1)(react@18.3.1) @@ -34,6 +64,9 @@ dependencies: version: 2.6.1 devDependencies: + '@types/chroma-js': + specifier: ^3.1.2 + version: 3.1.2 '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -482,6 +515,34 @@ packages: dev: true optional: true + /@floating-ui/core@1.7.5: + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + dependencies: + '@floating-ui/utils': 0.2.11 + dev: false + + /@floating-ui/dom@1.7.6: + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + dev: false + + /@floating-ui/react-dom@2.1.8(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.7.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@floating-ui/utils@0.2.11: + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + dev: false + /@jridgewell/gen-mapping@0.3.13: resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} dependencies: @@ -533,6 +594,635 @@ packages: fastq: 1.20.1 dev: true + /@radix-ui/number@1.1.1: + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + dev: false + + /@radix-ui/primitive@1.1.3: + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + dev: false + + /@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + 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 + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-context@1.1.2(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + 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 + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + dev: false + + /@radix-ui/react-direction@1.1.1(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + 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 + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + 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 + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + 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 + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-id@1.1.1(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-label@2.1.8(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + 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 + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + 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 + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + dev: false + + /@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + 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 + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.2(@types/react@18.3.28)(react@18.3.1) + dev: false + + /@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + 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 + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + 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 + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + 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 + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-primitive@2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + 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 + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + 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 + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + 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 + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-slot@1.2.3(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-slot@1.2.4(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-rect@1.1.1(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/rect': 1.1.1 + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-size@1.1.1(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@types/react': 18.3.28 + react: 18.3.1 + dev: false + + /@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} + 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 + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/rect@1.1.1: + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + dev: false + /@rolldown/pluginutils@1.0.0-beta.27: resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} dev: true @@ -766,6 +1456,10 @@ packages: '@babel/types': 7.29.0 dev: true + /@types/chroma-js@3.1.2: + resolution: {integrity: sha512-YBTQqArPN8A0niHXCwrO1z5x++a+6l0mLBykncUpr23oIPW7L4h39s6gokdK/bDrPmSh8+TjMmrhBPnyiaWPmQ==} + dev: true + /@types/estree@1.0.8: resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} dev: true @@ -778,7 +1472,6 @@ packages: /@types/prop-types@15.7.15: resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - dev: true /@types/react-dom@18.3.7(@types/react@18.3.28): resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} @@ -786,14 +1479,12 @@ packages: '@types/react': ^18.0.0 dependencies: '@types/react': 18.3.28 - dev: true /@types/react@18.3.28: resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} dependencies: '@types/prop-types': 15.7.15 csstype: 3.2.3 - dev: true /@vitejs/plugin-react@4.7.0(vite@6.4.2): resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} @@ -832,6 +1523,13 @@ packages: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} dev: true + /aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + dependencies: + tslib: 2.8.1 + dev: false + /autoprefixer@10.5.0(postcss@8.5.12): resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} @@ -901,17 +1599,42 @@ packages: fsevents: 2.3.3 dev: true + /chroma-js@3.2.0: + resolution: {integrity: sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==} + dev: false + /class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} dependencies: clsx: 2.1.1 dev: false + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + dev: false + /clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} dev: false + /cmdk@1.1.1(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.28)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7)(@types/react@18.3.28)(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + dev: false + /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -934,7 +1657,6 @@ packages: /csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - dev: true /debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -948,6 +1670,10 @@ packages: ms: 2.1.3 dev: true + /detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -1082,6 +1808,11 @@ packages: engines: {node: '>=6.9.0'} dev: true + /get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1362,11 +2093,54 @@ packages: scheduler: 0.23.2 dev: false + /react-loading-skeleton@3.5.0(react@18.3.1): + resolution: {integrity: sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.3.1 + dev: false + /react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} dev: true + /react-remove-scroll-bar@2.3.8(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + dev: false + + /react-remove-scroll@2.7.2(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.28)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.28)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.28)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.28)(react@18.3.1) + dev: false + /react-router-dom@7.14.2(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} engines: {node: '>=20.0.0'} @@ -1395,6 +2169,22 @@ packages: set-cookie-parser: 2.7.2 dev: false + /react-style-singleton@2.2.3(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + dev: false + /react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -1612,6 +2402,37 @@ packages: picocolors: 1.1.1 dev: true + /use-callback-ref@1.3.3(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + react: 18.3.1 + tslib: 2.8.1 + dev: false + + /use-sidecar@1.1.3(@types/react@18.3.28)(react@18.3.1): + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.28 + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true diff --git a/client/ui-wails/frontend/src/App.tsx b/client/ui-wails/frontend/src/App.tsx deleted file mode 100644 index 332b23cb9..000000000 --- a/client/ui-wails/frontend/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import "./globals.css"; -import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; -import QuickActions from "@/screens/QuickActions.tsx"; -import LoginUrl from "@/screens/LoginUrl.tsx"; -import Update from "@/screens/Update.tsx"; -import Layout from "@/layout.tsx"; -import Peers from "@/screens/Peers.tsx"; -import Networks from "@/screens/Networks.tsx"; -import Profiles from "@/screens/Profiles.tsx"; -import Settings from "@/screens/Settings.tsx"; -import Debug from "@/screens/Debug.tsx"; -import {Main} from "@/screens/Main.tsx"; - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - , -); diff --git a/client/ui-wails/frontend/src/app.tsx b/client/ui-wails/frontend/src/app.tsx new file mode 100644 index 000000000..c861a3a4b --- /dev/null +++ b/client/ui-wails/frontend/src/app.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./globals.css"; +import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; +import QuickActions from "@/screens/QuickActions.tsx"; +import LoginUrl from "@/screens/LoginUrl.tsx"; +import Update from "@/screens/Update.tsx"; +import Layout from "@/layout.tsx"; +import Peers from "@/screens/Peers.tsx"; +import Networks from "@/screens/Networks.tsx"; +import Profiles from "@/screens/Profiles.tsx"; +import Settings from "@/screens/Settings.tsx"; +import Debug from "@/screens/Debug.tsx"; +import {Main} from "@/screens/Main.tsx"; +import { SkeletonTheme } from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + , +); diff --git a/client/ui-wails/frontend/src/components/Button.tsx b/client/ui-wails/frontend/src/components/Button.tsx index a1b2867b3..1d4c9135a 100644 --- a/client/ui-wails/frontend/src/components/Button.tsx +++ b/client/ui-wails/frontend/src/components/Button.tsx @@ -1,42 +1,151 @@ +import { cva, VariantProps } from "class-variance-authority"; +import classNames from "classnames"; import { ButtonHTMLAttributes, forwardRef } from "react"; -import { cn } from "../lib/cn"; -type Variant = "primary" | "secondary" | "ghost" | "danger"; -type Size = "sm" | "md"; +export type ButtonVariants = VariantProps; -const variants: Record = { - primary: "bg-netbird text-white hover:bg-netbird-500 disabled:bg-nb-gray-300", - secondary: - "bg-nb-gray-100 text-nb-gray-900 hover:bg-nb-gray-200 dark:bg-nb-gray-900 dark:text-nb-gray-50 dark:hover:bg-nb-gray-800", - ghost: - "bg-transparent text-nb-gray-700 hover:bg-nb-gray-100 dark:text-nb-gray-200 dark:hover:bg-nb-gray-900", - danger: "bg-red-600 text-white hover:bg-red-500", -}; - -const sizes: Record = { - sm: "h-7 px-2 text-xs", - md: "h-9 px-3 text-sm", -}; - -interface Props extends ButtonHTMLAttributes { - variant?: Variant; - size?: Size; +export interface ButtonProps + extends ButtonHTMLAttributes, + ButtonVariants { + disabled?: boolean; + stopPropagation?: boolean; } -export const Button = forwardRef(function Button( - { variant = "primary", size = "md", className, ...rest }, - ref, -) { - return ( - + ); + }, +); + +export default Button; diff --git a/client/ui-wails/frontend/src/components/Dialog.tsx b/client/ui-wails/frontend/src/components/Dialog.tsx new file mode 100644 index 000000000..15f6e727d --- /dev/null +++ b/client/ui-wails/frontend/src/components/Dialog.tsx @@ -0,0 +1,149 @@ +import { + forwardRef, + ComponentPropsWithoutRef, + ElementRef, + HTMLAttributes, +} from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; +import { X } from "lucide-react"; +import { cn } from "@/lib/cn"; + +export const Root = DialogPrimitive.Root; +export const Trigger = DialogPrimitive.Trigger; +export const Close = DialogPrimitive.Close; +export const Portal = DialogPrimitive.Portal; + +export const Overlay = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(function DialogOverlay({ className, ...props }, ref) { + return ( + + ); +}); + +type ContentProps = ComponentPropsWithoutRef & { + showClose?: boolean; + maxWidthClass?: string; +}; + +export const Content = forwardRef< + ElementRef, + ContentProps +>(function DialogContent( + { + className, + children, + showClose = true, + maxWidthClass = "max-w-md", + ...props + }, + ref, +) { + return ( + + + e.stopPropagation()} + {...props} + > + + Dialog + + {children} + {showClose && ( + + + + )} + + + + ); +}); + +export const Title = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(function DialogTitle({ className, ...props }, ref) { + return ( + + ); +}); + +export const Description = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(function DialogDescription({ className, ...props }, ref) { + return ( + + ); +}); + +type FooterProps = HTMLAttributes & { + separator?: boolean; +}; + +export const Footer = ({ + className, + separator = true, + ...props +}: FooterProps) => ( +
+
+
+); diff --git a/client/ui-wails/frontend/src/components/HelpText.tsx b/client/ui-wails/frontend/src/components/HelpText.tsx new file mode 100644 index 000000000..ef321349b --- /dev/null +++ b/client/ui-wails/frontend/src/components/HelpText.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from "react"; +import { cn } from "@/lib/cn"; + +type Props = { + children?: ReactNode; + margin?: boolean; + className?: string; +}; + +export const HelpText = ({ children, margin = true, className }: Props) => ( + + {children} + +); + +export default HelpText; diff --git a/client/ui-wails/frontend/src/components/IconButton.tsx b/client/ui-wails/frontend/src/components/IconButton.tsx new file mode 100644 index 000000000..efe000abf --- /dev/null +++ b/client/ui-wails/frontend/src/components/IconButton.tsx @@ -0,0 +1,41 @@ +import { ComponentType, forwardRef } from "react"; +import { motion, HTMLMotionProps } from "framer-motion"; +import { LucideProps } from "lucide-react"; +import { cn } from "@/lib/cn"; + +type Props = HTMLMotionProps<"button"> & { + icon: ComponentType; + iconSize?: number; + iconClassName?: string; +}; + +export const IconButton = forwardRef( + function IconButton( + { + icon: Icon, + iconSize = 18, + iconClassName, + className, + type = "button", + ...props + }, + ref, + ) { + return ( + + + + ); + }, +); diff --git a/client/ui-wails/frontend/src/components/Input.tsx b/client/ui-wails/frontend/src/components/Input.tsx index 03ddba586..040d0c2f4 100644 --- a/client/ui-wails/frontend/src/components/Input.tsx +++ b/client/ui-wails/frontend/src/components/Input.tsx @@ -1,33 +1,172 @@ -import { InputHTMLAttributes, forwardRef } from "react"; -import { cn } from "../lib/cn"; +import { cva, VariantProps } from "class-variance-authority"; +import { AlertCircle, Eye, EyeOff } from "lucide-react"; +import { + forwardRef, + InputHTMLAttributes, + ReactNode, + useId, + useState, +} from "react"; +import { cn } from "@/lib/cn"; +import { Label } from "@/components/Label"; -interface Props extends InputHTMLAttributes { - label?: string; +type InputVariants = VariantProps; + +export interface InputProps + extends InputHTMLAttributes, + InputVariants { + label?: string; + customPrefix?: ReactNode; + customSuffix?: ReactNode; + maxWidthClass?: string; + icon?: ReactNode; + error?: string; + prefixClassName?: string; + showPasswordToggle?: boolean; } -export const Input = forwardRef(function Input( - { label, className, id, ...rest }, - ref, -) { - const inputId = id ?? label?.toLowerCase().replace(/\s+/g, "-"); - return ( -
- {label && ( - - )} - -
- ); +const inputVariants = cva("", { + variants: { + variant: { + default: [ + "dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-nb-gray-700", + "ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10", + ], + darker: [ + "dark:bg-nb-gray-920 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-300 dark:border-nb-gray-800", + "ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20 focus-visible:ring-neutral-300/10", + ], + error: [ + "dark:bg-nb-gray-900 dark:placeholder:text-neutral-400/70 placeholder:text-neutral-500 border-neutral-200 dark:border-red-500 text-red-500", + "ring-offset-red-500/10 dark:ring-offset-red-500/10 dark:focus-visible:ring-red-500/10 focus-visible:ring-red-500/10", + ], + }, + prefixSuffixVariant: { + default: [ + "dark:bg-nb-gray-900 border-neutral-200 dark:border-nb-gray-700 text-nb-gray-300", + ], + error: [ + "dark:bg-nb-gray-900 border-red-500 text-nb-gray-300 text-red-500", + ], + }, + }, }); + +export const Input = forwardRef(function Input( + { + className, + type, + label, + customSuffix, + customPrefix, + icon, + maxWidthClass = "", + error, + variant = "default", + prefixClassName, + showPasswordToggle = false, + id, + ...props + }, + ref, +) { + const [showPassword, setShowPassword] = useState(false); + const isPasswordType = type === "password"; + const inputType = isPasswordType && showPassword ? "text" : type; + + const reactId = useId(); + const inputId = + id ?? (label ? `input-${reactId}` : undefined); + + const passwordToggle = + isPasswordType && showPasswordToggle ? ( + + ) : null; + + const suffix = passwordToggle || customSuffix; + + return ( +
+ {label && } +
+ {customPrefix && ( +
+ {customPrefix} +
+ )} + + {icon && ( +
+ {icon} +
+ )} + + + + {suffix && ( +
+ {suffix} +
+ )} +
+ {error && ( + + + {error} + + )} +
+ ); +}); + +export default Input; diff --git a/client/ui-wails/frontend/src/components/Label.tsx b/client/ui-wails/frontend/src/components/Label.tsx new file mode 100644 index 000000000..3850d5234 --- /dev/null +++ b/client/ui-wails/frontend/src/components/Label.tsx @@ -0,0 +1,40 @@ +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; +import { ComponentPropsWithoutRef, forwardRef, Ref } from "react"; +import { cn } from "@/lib/cn"; + +const labelVariants = cva( + "text-sm font-medium tracking-wider leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 mb-1.5 inline-block dark:text-nb-gray-200 flex items-center gap-2", +); + +type LabelProps = ComponentPropsWithoutRef & + VariantProps & { + as?: "label" | "div"; + }; + +export const Label = forwardRef(function Label( + { className, as = "label", children, ...props }, + ref, +) { + const classes = cn(labelVariants(), className, "select-none"); + + if (as === "div") { + return ( +
} className={classes}> + {children} +
+ ); + } + + return ( + } + className={classes} + {...props} + > + {children} + + ); +}); + +export default Label; diff --git a/client/ui-wails/frontend/src/components/NavItem.tsx b/client/ui-wails/frontend/src/components/NavItem.tsx new file mode 100644 index 000000000..276f2e888 --- /dev/null +++ b/client/ui-wails/frontend/src/components/NavItem.tsx @@ -0,0 +1,81 @@ +import { ComponentType, forwardRef } from "react"; +import { motion, HTMLMotionProps } from "framer-motion"; +import { LucideProps } from "lucide-react"; +import { cn } from "@/lib/cn"; + +type Props = HTMLMotionProps<"button"> & { + icon: ComponentType; + title: string; + description?: string; + active?: boolean; + iconSize?: number; +}; + +export const NavItem = forwardRef( + function NavItem( + { + icon: Icon, + title, + description, + active = false, + iconSize = 15, + className, + type = "button", + ...props + }, + ref, + ) { + return ( + +
+ +
+
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+
+ ); + }, +); diff --git a/client/ui-wails/frontend/src/components/NetBirdConnectToggle.tsx b/client/ui-wails/frontend/src/components/NetBirdConnectToggle.tsx index d5bc83b0b..29fff6f0f 100644 --- a/client/ui-wails/frontend/src/components/NetBirdConnectToggle.tsx +++ b/client/ui-wails/frontend/src/components/NetBirdConnectToggle.tsx @@ -46,7 +46,7 @@ export const NetBirdConnectToggle = ({ state, size = 140, onClick }: NetBirdConn return (
void; + onCreate: (name: string) => void; +}; + +export const NewProfileDialog = ({ open, onOpenChange, onCreate }: Props) => { + const [name, setName] = useState(""); + + useEffect(() => { + if (!open) setName(""); + }, [open]); + + const trimmed = name.trim(); + const canSubmit = trimmed.length > 0; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + if (!canSubmit) return; + onCreate(trimmed); + onOpenChange(false); + }; + + return ( + + e.preventDefault()} + > +
+
+ New Profile + + Profiles let you keep separate NetBird connections + side by side. Give your profile a memorable name. + +
+ +
+ setName(e.target.value)} + /> +
+ + + + + +
+
+
+ ); +}; diff --git a/client/ui-wails/frontend/src/components/PlaceholderHeader.tsx b/client/ui-wails/frontend/src/components/PlaceholderHeader.tsx index e390f9cd5..33b08da17 100644 --- a/client/ui-wails/frontend/src/components/PlaceholderHeader.tsx +++ b/client/ui-wails/frontend/src/components/PlaceholderHeader.tsx @@ -1,7 +1,7 @@ export default function PlaceholderHeader() { return (
); diff --git a/client/ui-wails/frontend/src/components/ProfileSelector.tsx b/client/ui-wails/frontend/src/components/ProfileSelector.tsx new file mode 100644 index 000000000..69a13e4f4 --- /dev/null +++ b/client/ui-wails/frontend/src/components/ProfileSelector.tsx @@ -0,0 +1,407 @@ +import { useState } from "react"; +import * as Popover from "@radix-ui/react-popover"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { Command } from "cmdk"; +import { Dialogs } from "@wailsio/runtime"; +import { + ChevronDown, + MoreVertical, + PlusCircle, + Search, + Trash2, + UserMinus, +} from "lucide-react"; +import { cn } from "@/lib/cn"; +import { generateColorFromString } from "@/lib/color"; +import { NewProfileDialog } from "@/components/NewProfileDialog"; + +export type Profile = { + id: string; + name: string; +}; + +const MOCK_PROFILES: Profile[] = [ + { id: "default", name: "Default Profile" }, + { id: "work", name: "Work" }, + { id: "personal", name: "Personal" }, + { id: "staging", name: "Staging" }, + { id: "production", name: "Production" }, + { id: "dev", name: "Development" }, + { id: "qa", name: "QA Environment" }, + { id: "demo", name: "Demo" }, + { id: "client-acme", name: "Client - ACME" }, + { id: "client-globex", name: "Client - Globex" }, + { id: "client-initech", name: "Client - Initech" }, + { id: "homelab", name: "Homelab" }, + { id: "office-berlin", name: "Office Berlin" }, + { id: "office-sf", name: "Office San Francisco" }, + { id: "office-tokyo", name: "Office Tokyo" }, + { id: "vpn-eu", name: "VPN EU" }, + { id: "vpn-us", name: "VPN US" }, + { id: "vpn-asia", name: "VPN Asia" }, + { id: "test", name: "Test" }, + { id: "sandbox", name: "Sandbox" }, +]; + +type Props = { + email?: string; +}; + +export const ProfileSelector = ({ email = "" }: Props) => { + const [profiles, setProfiles] = useState(MOCK_PROFILES); + const [selectedId, setSelectedId] = useState(MOCK_PROFILES[0].id); + const [open, setOpen] = useState(false); + const [newOpen, setNewOpen] = useState(false); + + const selected = profiles.find((p) => p.id === selectedId) ?? profiles[0]; + + const sorted = [...profiles].sort((a, b) => + a.name.localeCompare(b.name), + ); + + const handleSelect = (id: string) => { + setSelectedId(id); + setOpen(false); + }; + + const handleDeregister = async (id: string) => { + const profile = profiles.find((p) => p.id === id); + if (!profile) return; + const result = await Dialogs.Warning({ + Title: "Deregister Profile", + Message: `Are you sure you want to deregister "${profile.name}"? You will need to log in again to use it.`, + Buttons: [ + { Label: "Cancel", IsCancel: true }, + { Label: "Deregister", IsDefault: true }, + ], + }); + if (result !== "Deregister") return; + console.log("Deregister profile", id); + }; + + const handleDelete = async (id: string) => { + const profile = profiles.find((p) => p.id === id); + if (!profile) return; + const result = await Dialogs.Warning({ + Title: "Delete Profile", + Message: `Are you sure you want to delete "${profile.name}"? This action cannot be undone.`, + Buttons: [ + { Label: "Cancel", IsCancel: true }, + { Label: "Delete", IsDefault: true }, + ], + }); + if (result !== "Delete") return; + setProfiles((prev) => prev.filter((p) => p.id !== id)); + if (selectedId === id) { + const remaining = profiles.filter((p) => p.id !== id); + if (remaining.length > 0) setSelectedId(remaining[0].id); + } + }; + + const handleNewProfile = () => { + setOpen(false); + setNewOpen(true); + }; + + const handleCreateProfile = (name: string) => { + const id = `${name.toLowerCase().replace(/\s+/g, "-")}-${Date.now()}`; + setProfiles((prev) => [...prev, { id, name }]); + setSelectedId(id); + }; + + const initial = selected?.name.charAt(0).toUpperCase() ?? "?"; + const initialColor = generateColorFromString(selected?.name); + + return ( + <> + + + + + + + e.preventDefault()} + > + +
+
+ + +
+
+ + + + + +
+

+ No Profiles Found +

+

+ Try a different search term + or create a new profile. +

+
+
+ + {sorted.map((profile) => ( + + handleSelect(profile.id) + } + onDeregister={() => + handleDeregister(profile.id) + } + onDelete={() => + handleDelete(profile.id) + } + /> + ))} +
+
+ + + +
+ +
+ + + + + + + + + ); +}; + +type ProfileRowProps = { + profile: Profile; + selected: boolean; + onSelect: () => void; + onDeregister: () => void; + onDelete: () => void; +}; + +const ProfileRow = ({ + profile, + selected, + onSelect, + onDeregister, + onDelete, +}: ProfileRowProps) => { + const [menuOpen, setMenuOpen] = useState(false); + const initial = profile.name.charAt(0).toUpperCase(); + const initialColor = generateColorFromString(profile.name); + + return ( + onSelect()} + className={cn( + "group flex items-center gap-2 pl-2 pr-3 py-1.5 rounded-md cursor-default outline-none", + "data-[selected=true]:bg-nb-gray-920", + selected && "bg-nb-gray-920", + )} + > +
+ {initial} +
+ + {profile.name} + + + + + + + + e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + className={cn( + "w-44 rounded-md border border-nb-gray-920 bg-nb-gray-935 shadow-lg p-1 z-50", + )} + > + { + e.preventDefault(); + onDeregister(); + setMenuOpen(false); + }} + className={cn( + "flex items-center gap-2 px-2 py-1.5 rounded-md cursor-default outline-none", + "text-xs text-nb-gray-200 data-[highlighted]:bg-nb-gray-920", + )} + > + + Deregister + + { + e.preventDefault(); + onDelete(); + setMenuOpen(false); + }} + className={cn( + "flex items-center gap-2 px-2 py-1.5 rounded-md cursor-default outline-none", + "text-xs text-red-500 data-[highlighted]:bg-nb-gray-920", + )} + > + + Delete Profile + + + + +
+ ); +}; diff --git a/client/ui-wails/frontend/src/components/SearchInput.tsx b/client/ui-wails/frontend/src/components/SearchInput.tsx new file mode 100644 index 000000000..af1918251 --- /dev/null +++ b/client/ui-wails/frontend/src/components/SearchInput.tsx @@ -0,0 +1,30 @@ +import { forwardRef, InputHTMLAttributes } from "react"; +import { SearchIcon } from "lucide-react"; +import { cn } from "@/lib/cn"; + +type Props = InputHTMLAttributes & { + iconSize?: number; +}; + +export const SearchInput = forwardRef( + function SearchInput({ iconSize = 14, className, ...props }, ref) { + return ( +
+ + +
+ ); + }, +); diff --git a/client/ui-wails/frontend/src/globals.css b/client/ui-wails/frontend/src/globals.css index 283464560..62719ed62 100644 --- a/client/ui-wails/frontend/src/globals.css +++ b/client/ui-wails/frontend/src/globals.css @@ -13,6 +13,7 @@ html, body, #root { height: 100%; + overflow: hidden; } body { diff --git a/client/ui-wails/frontend/src/Layout.tsx b/client/ui-wails/frontend/src/layout.tsx similarity index 100% rename from client/ui-wails/frontend/src/Layout.tsx rename to client/ui-wails/frontend/src/layout.tsx diff --git a/client/ui-wails/frontend/src/layouts/ConnectionStatus.tsx b/client/ui-wails/frontend/src/layouts/ConnectionStatus.tsx new file mode 100644 index 000000000..d431020f2 --- /dev/null +++ b/client/ui-wails/frontend/src/layouts/ConnectionStatus.tsx @@ -0,0 +1,25 @@ +import { + ConnectionState, + NetBirdConnectToggle, +} from "@/components/NetBirdConnectToggle.tsx"; + +export const ConnectionStatus = () => { + return ( +
+ +

+ Connected +

+

+ peer-hostname.netbird.cloud +

+

+ 192.168.0.1 +

+
+ ); +}; diff --git a/client/ui-wails/frontend/src/layouts/Header.tsx b/client/ui-wails/frontend/src/layouts/Header.tsx new file mode 100644 index 000000000..52f63a8c2 --- /dev/null +++ b/client/ui-wails/frontend/src/layouts/Header.tsx @@ -0,0 +1,12 @@ +import { ProfileSelector } from "@/components/ProfileSelector.tsx"; +import { IconButton } from "@/components/IconButton.tsx"; +import { SettingsIcon } from "lucide-react"; + +export const Header = () => { + return ( +
+ + +
+ ); +}; diff --git a/client/ui-wails/frontend/src/layouts/Navigation.tsx b/client/ui-wails/frontend/src/layouts/Navigation.tsx new file mode 100644 index 000000000..558f56241 --- /dev/null +++ b/client/ui-wails/frontend/src/layouts/Navigation.tsx @@ -0,0 +1,31 @@ +import { NavItem } from "@/components/NavItem.tsx"; +import { + Layers3Icon, + MonitorSmartphoneIcon, + SquareArrowUpRight, +} from "lucide-react"; + +export const Navigation = () => { + return ( + + ); +}; diff --git a/client/ui-wails/frontend/src/lib/color.ts b/client/ui-wails/frontend/src/lib/color.ts new file mode 100644 index 000000000..4375bc7e6 --- /dev/null +++ b/client/ui-wails/frontend/src/lib/color.ts @@ -0,0 +1,17 @@ +import chroma from "chroma-js"; + +export const generateColorFromString = (str?: string) => { + if (!str) return "#f68330"; + if (str.includes("System")) return "#808080"; + if (str.toLowerCase().startsWith("netbird")) return "#f68330"; + let hash = 0; + str.split("").forEach((char) => { + hash = char.charCodeAt(0) + ((hash << 5) - hash); + }); + let colour = "#"; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + colour += value.toString(16).padStart(2, "0"); + } + return chroma(colour).saturate(2).luminance(0.4).hex(); +}; diff --git a/client/ui-wails/frontend/src/modules/peers/PeerFilters.tsx b/client/ui-wails/frontend/src/modules/peers/PeerFilters.tsx new file mode 100644 index 000000000..2dbead972 --- /dev/null +++ b/client/ui-wails/frontend/src/modules/peers/PeerFilters.tsx @@ -0,0 +1,53 @@ +import { cn } from "@/lib/cn"; + +export type StatusFilter = "all" | "online" | "offline"; + +const FILTERS: { value: StatusFilter; label: string }[] = [ + { value: "all", label: "All" }, + { value: "online", label: "Online" }, + { value: "offline", label: "Offline" }, +]; + +type Props = { + value: StatusFilter; + onChange: (value: StatusFilter) => void; + counts: Record; +}; + +export const PeerFilters = ({ value, onChange, counts }: Props) => { + return ( +
+ {FILTERS.map((f) => { + const active = value === f.value; + return ( + + ); + })} +
+ ); +}; diff --git a/client/ui-wails/frontend/src/modules/peers/PeersList.tsx b/client/ui-wails/frontend/src/modules/peers/PeersList.tsx new file mode 100644 index 000000000..c42ad6b7d --- /dev/null +++ b/client/ui-wails/frontend/src/modules/peers/PeersList.tsx @@ -0,0 +1,51 @@ +import { cn } from "@/lib/cn"; +import { Peer, PeerStatus } from "./types"; + +const DOT: Record = { + connected: "bg-green-400", + connecting: "bg-yellow-300 animate-pulse-slow", + disconnected: "bg-nb-gray-500", +}; + +export const PeersList = ({ data }: { data: Peer[] }) => { + if (data.length === 0) { + return ( +
+ No peers match the current filters. +
+ ); + } + + return ( +
    + {data.map((peer) => ( +
  • + + + {peer.fqdn} + + + {peer.ip} + +
  • + ))} +
+ ); +}; diff --git a/client/ui-wails/frontend/src/modules/peers/PeersModule.tsx b/client/ui-wails/frontend/src/modules/peers/PeersModule.tsx new file mode 100644 index 000000000..328ba4ccf --- /dev/null +++ b/client/ui-wails/frontend/src/modules/peers/PeersModule.tsx @@ -0,0 +1,73 @@ +import { useMemo, useState } from "react"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { cn } from "@/lib/cn"; +import { SearchInput } from "@/components/SearchInput"; +import { mockPeers } from "./mockPeers"; +import { PeerFilters, StatusFilter } from "./PeerFilters"; +import { PeersList } from "./PeersList"; + +const isOnline = (status: string) => status === "connected"; + +export const PeersModule = () => { + const [search, setSearch] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + + const counts = useMemo>(() => { + const online = mockPeers.filter((p) => isOnline(p.status)).length; + return { + all: mockPeers.length, + online, + offline: mockPeers.length - online, + }; + }, []); + + const filtered = useMemo(() => { + const q = search.trim().toLowerCase(); + return mockPeers.filter((p) => { + if (statusFilter === "online" && !isOnline(p.status)) return false; + if (statusFilter === "offline" && isOnline(p.status)) return false; + if (q && !p.fqdn.toLowerCase().includes(q) && !p.ip.includes(q)) { + return false; + } + return true; + }); + }, [search, statusFilter]); + + return ( +
+
+ setSearch(e.target.value)} + /> + +
+ + + + + + + + +
+ ); +}; diff --git a/client/ui-wails/frontend/src/modules/peers/mockPeers.ts b/client/ui-wails/frontend/src/modules/peers/mockPeers.ts new file mode 100644 index 000000000..fd1e0527f --- /dev/null +++ b/client/ui-wails/frontend/src/modules/peers/mockPeers.ts @@ -0,0 +1,385 @@ +import { Peer } from "./types"; + +const minutesAgo = (m: number) => new Date(Date.now() - m * 60 * 1000); + +export const mockPeers: Peer[] = [ + { + id: "p-001", + fqdn: "alice-laptop.netbird.cloud", + ip: "100.64.0.12", + status: "connected", + lastHandshake: minutesAgo(1), + latencyMs: 18, + relayed: false, + iceLocalCandidate: "srflx", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 84, + bytesTx: 1024 * 1024 * 12, + endpointLocal: "192.168.1.24:51820", + endpointRemote: "203.0.113.45:51820", + }, + { + id: "p-002", + fqdn: "bob-desktop.netbird.cloud", + ip: "100.64.0.21", + status: "connected", + lastHandshake: minutesAgo(3), + latencyMs: 42, + relayed: true, + relayAddress: "rel.eu-central.netbird.io:443", + iceLocalCandidate: "relay", + iceRemoteCandidate: "relay", + bytesRx: 1024 * 380, + bytesTx: 1024 * 940, + endpointLocal: "10.0.0.8:51820", + endpointRemote: "198.51.100.7:51820", + }, + { + id: "p-003", + fqdn: "build-runner-01.netbird.cloud", + ip: "100.64.0.34", + status: "connecting", + lastHandshake: minutesAgo(15), + latencyMs: 0, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 0, + bytesTx: 0, + endpointLocal: "192.168.1.45:51820", + endpointRemote: "—", + }, + { + id: "p-004", + fqdn: "carol-phone.netbird.cloud", + ip: "100.64.0.55", + status: "disconnected", + lastHandshake: minutesAgo(620), + latencyMs: 0, + relayed: false, + iceLocalCandidate: "srflx", + iceRemoteCandidate: "srflx", + bytesRx: 1024 * 1024 * 5, + bytesTx: 1024 * 1024 * 2, + endpointLocal: "—", + endpointRemote: "—", + }, + { + id: "p-005", + fqdn: "exit-berlin.netbird.cloud", + ip: "100.64.0.2", + status: "connected", + lastHandshake: minutesAgo(0.2), + latencyMs: 9, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "srflx", + bytesRx: 1024 * 1024 * 1024 * 2, + bytesTx: 1024 * 1024 * 512, + endpointLocal: "10.10.0.4:51820", + endpointRemote: "203.0.113.99:51820", + }, + { + id: "p-006", + fqdn: "db-replica-eu.netbird.cloud", + ip: "100.64.0.78", + status: "connected", + lastHandshake: minutesAgo(7), + latencyMs: 64, + relayed: true, + relayAddress: "rel.us-east.netbird.io:443", + iceLocalCandidate: "relay", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 240, + bytesTx: 1024 * 1024 * 90, + endpointLocal: "172.16.0.10:51820", + endpointRemote: "198.51.100.42:51820", + }, + { + id: "p-007", + fqdn: "dev-vm-mac.netbird.cloud", + ip: "100.64.0.91", + status: "disconnected", + lastHandshake: minutesAgo(2880), + latencyMs: 0, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 0, + bytesTx: 0, + endpointLocal: "—", + endpointRemote: "—", + }, + { + id: "p-008", + fqdn: "ci-worker-03.netbird.cloud", + ip: "100.64.0.103", + status: "connected", + lastHandshake: minutesAgo(0.5), + latencyMs: 27, + relayed: false, + iceLocalCandidate: "prflx", + iceRemoteCandidate: "srflx", + bytesRx: 1024 * 1024 * 14, + bytesTx: 1024 * 1024 * 3, + endpointLocal: "192.168.50.7:51820", + endpointRemote: "203.0.113.61:51820", + }, + { + id: "p-009", + fqdn: "k8s-control-plane.netbird.cloud", + ip: "100.64.0.110", + status: "connected", + lastHandshake: minutesAgo(2), + latencyMs: 12, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 410, + bytesTx: 1024 * 1024 * 380, + endpointLocal: "10.0.1.10:51820", + endpointRemote: "10.0.1.11:51820", + }, + { + id: "p-010", + fqdn: "k8s-worker-01.netbird.cloud", + ip: "100.64.0.111", + status: "connected", + lastHandshake: minutesAgo(2), + latencyMs: 14, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 220, + bytesTx: 1024 * 1024 * 190, + endpointLocal: "10.0.1.20:51820", + endpointRemote: "10.0.1.21:51820", + }, + { + id: "p-011", + fqdn: "k8s-worker-02.netbird.cloud", + ip: "100.64.0.112", + status: "connecting", + lastHandshake: minutesAgo(8), + latencyMs: 0, + relayed: false, + iceLocalCandidate: "srflx", + iceRemoteCandidate: "srflx", + bytesRx: 0, + bytesTx: 0, + endpointLocal: "10.0.1.22:51820", + endpointRemote: "—", + }, + { + id: "p-012", + fqdn: "monitoring-prom.netbird.cloud", + ip: "100.64.0.130", + status: "connected", + lastHandshake: minutesAgo(0.3), + latencyMs: 22, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "srflx", + bytesRx: 1024 * 1024 * 56, + bytesTx: 1024 * 1024 * 18, + endpointLocal: "10.20.0.5:51820", + endpointRemote: "203.0.113.122:51820", + }, + { + id: "p-013", + fqdn: "grafana.netbird.cloud", + ip: "100.64.0.131", + status: "connected", + lastHandshake: minutesAgo(0.4), + latencyMs: 19, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "srflx", + bytesRx: 1024 * 1024 * 32, + bytesTx: 1024 * 1024 * 8, + endpointLocal: "10.20.0.6:51820", + endpointRemote: "203.0.113.123:51820", + }, + { + id: "p-014", + fqdn: "loki-log-aggregator.netbird.cloud", + ip: "100.64.0.132", + status: "disconnected", + lastHandshake: minutesAgo(45), + latencyMs: 0, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 12, + bytesTx: 1024 * 1024 * 4, + endpointLocal: "—", + endpointRemote: "—", + }, + { + id: "p-015", + fqdn: "dave-laptop.netbird.cloud", + ip: "100.64.0.140", + status: "connected", + lastHandshake: minutesAgo(1), + latencyMs: 38, + relayed: true, + relayAddress: "rel.eu-west.netbird.io:443", + iceLocalCandidate: "relay", + iceRemoteCandidate: "relay", + bytesRx: 1024 * 720, + bytesTx: 1024 * 410, + endpointLocal: "192.168.43.21:51820", + endpointRemote: "198.51.100.88:51820", + }, + { + id: "p-016", + fqdn: "eve-iphone.netbird.cloud", + ip: "100.64.0.150", + status: "connecting", + lastHandshake: minutesAgo(20), + latencyMs: 0, + relayed: false, + iceLocalCandidate: "srflx", + iceRemoteCandidate: "host", + bytesRx: 0, + bytesTx: 0, + endpointLocal: "—", + endpointRemote: "—", + }, + { + id: "p-017", + fqdn: "frank-windows.netbird.cloud", + ip: "100.64.0.155", + status: "connected", + lastHandshake: minutesAgo(4), + latencyMs: 76, + relayed: false, + iceLocalCandidate: "srflx", + iceRemoteCandidate: "srflx", + bytesRx: 1024 * 1024 * 6, + bytesTx: 1024 * 1024 * 2, + endpointLocal: "192.168.1.55:51820", + endpointRemote: "203.0.113.200:51820", + }, + { + id: "p-018", + fqdn: "exit-frankfurt.netbird.cloud", + ip: "100.64.0.3", + status: "connected", + lastHandshake: minutesAgo(0.1), + latencyMs: 6, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 1024 * 5, + bytesTx: 1024 * 1024 * 1024 * 1, + endpointLocal: "10.10.0.5:51820", + endpointRemote: "203.0.113.150:51820", + }, + { + id: "p-019", + fqdn: "exit-singapore.netbird.cloud", + ip: "100.64.0.4", + status: "disconnected", + lastHandshake: minutesAgo(180), + latencyMs: 0, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 880, + bytesTx: 1024 * 1024 * 220, + endpointLocal: "—", + endpointRemote: "—", + }, + { + id: "p-020", + fqdn: "nas-home.netbird.cloud", + ip: "100.64.0.180", + status: "connected", + lastHandshake: minutesAgo(0.7), + latencyMs: 31, + relayed: false, + iceLocalCandidate: "srflx", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 1024 * 3, + bytesTx: 1024 * 1024 * 480, + endpointLocal: "192.168.0.50:51820", + endpointRemote: "203.0.113.45:51820", + }, + { + id: "p-021", + fqdn: "raspberrypi-iot.netbird.cloud", + ip: "100.64.0.181", + status: "connected", + lastHandshake: minutesAgo(5), + latencyMs: 54, + relayed: true, + relayAddress: "rel.eu-central.netbird.io:443", + iceLocalCandidate: "relay", + iceRemoteCandidate: "host", + bytesRx: 1024 * 240, + bytesTx: 1024 * 110, + endpointLocal: "192.168.0.121:51820", + endpointRemote: "198.51.100.42:51820", + }, + { + id: "p-022", + fqdn: "staging-api.netbird.cloud", + ip: "100.64.0.200", + status: "connected", + lastHandshake: minutesAgo(0.2), + latencyMs: 16, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 92, + bytesTx: 1024 * 1024 * 140, + endpointLocal: "10.30.0.10:51820", + endpointRemote: "10.30.0.11:51820", + }, + { + id: "p-023", + fqdn: "prod-api-eu.netbird.cloud", + ip: "100.64.0.201", + status: "connected", + lastHandshake: minutesAgo(0.1), + latencyMs: 8, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 1024 * 1024 * 1024 * 12, + bytesTx: 1024 * 1024 * 1024 * 3, + endpointLocal: "10.40.0.10:51820", + endpointRemote: "10.40.0.11:51820", + }, + { + id: "p-024", + fqdn: "prod-api-us.netbird.cloud", + ip: "100.64.0.202", + status: "connected", + lastHandshake: minutesAgo(0.1), + latencyMs: 92, + relayed: false, + iceLocalCandidate: "srflx", + iceRemoteCandidate: "srflx", + bytesRx: 1024 * 1024 * 1024 * 8, + bytesTx: 1024 * 1024 * 1024 * 2, + endpointLocal: "10.50.0.10:51820", + endpointRemote: "203.0.113.210:51820", + }, + { + id: "p-025", + fqdn: "old-jenkins.netbird.cloud", + ip: "100.64.0.220", + status: "disconnected", + lastHandshake: minutesAgo(8640), + latencyMs: 0, + relayed: false, + iceLocalCandidate: "host", + iceRemoteCandidate: "host", + bytesRx: 0, + bytesTx: 0, + endpointLocal: "—", + endpointRemote: "—", + }, +]; diff --git a/client/ui-wails/frontend/src/modules/peers/types.ts b/client/ui-wails/frontend/src/modules/peers/types.ts new file mode 100644 index 000000000..226b77536 --- /dev/null +++ b/client/ui-wails/frontend/src/modules/peers/types.ts @@ -0,0 +1,20 @@ +export type PeerStatus = "connected" | "connecting" | "disconnected"; + +export type IceCandidateType = "host" | "srflx" | "relay" | "prflx"; + +export type Peer = { + id: string; + fqdn: string; + ip: string; + status: PeerStatus; + lastHandshake: Date; + latencyMs: number; + relayed: boolean; + relayAddress?: string; + iceLocalCandidate: IceCandidateType; + iceRemoteCandidate: IceCandidateType; + bytesRx: number; + bytesTx: number; + endpointLocal: string; + endpointRemote: string; +}; diff --git a/client/ui-wails/frontend/src/screens/Main.tsx b/client/ui-wails/frontend/src/screens/Main.tsx index 87c6e9b25..d6a6a1850 100644 --- a/client/ui-wails/frontend/src/screens/Main.tsx +++ b/client/ui-wails/frontend/src/screens/Main.tsx @@ -1,76 +1,22 @@ -import {ConnectionState, NetBirdConnectToggle} from "@/components/NetBirdConnectToggle.tsx"; -import { - BoltIcon, - ChevronDown, - CircleUserRound, - Layers3Icon, - MonitorSmartphoneIcon, SettingsIcon, - SquareArrowUpRight -} from "lucide-react"; +import { ConnectionStatus } from "@/layouts/ConnectionStatus.tsx"; +import { Header } from "@/layouts/Header.tsx"; +import { Navigation } from "@/layouts/Navigation.tsx"; +import { PeersModule } from "@/modules/peers/PeersModule.tsx"; type Props = { }; export const Main = ({}: Props) => { return ( -
-
-
-
-
-
- D -
-
- Default - eduard@netbird.io -
- - -
-
-
-
- -
-
-
- -

Connected

-

peer-hostname.netbird.cloud

-

192.168.0.1

- +
+
+
+ +
-
+
+
diff --git a/client/ui-wails/frontend/tailwind.config.ts b/client/ui-wails/frontend/tailwind.config.ts index 78dcc3a07..93ff39eea 100644 --- a/client/ui-wails/frontend/tailwind.config.ts +++ b/client/ui-wails/frontend/tailwind.config.ts @@ -34,6 +34,102 @@ const config: Config = { 950: "#181a1d", 960: "#16181b", }, + gray: { + 50: "#F9FAFB", + 100: "#F3F4F6", + 200: "#E5E7EB", + 300: "#D1D5DB", + 400: "#9CA3AF", + 500: "#6B7280", + 600: "#4B5563", + 700: "#374151", + 800: "#1F2937", + 900: "#111827", + }, + red: { + 50: "#FDF2F2", + 100: "#FDE8E8", + 200: "#FBD5D5", + 300: "#F8B4B4", + 400: "#F98080", + 500: "#F05252", + 600: "#E02424", + 700: "#C81E1E", + 800: "#9B1C1C", + 900: "#771D1D", + }, + yellow: { + 50: "#FDFDEA", + 100: "#FDF6B2", + 200: "#FCE96A", + 300: "#FACA15", + 400: "#E3A008", + 500: "#C27803", + 600: "#9F580A", + 700: "#8E4B10", + 800: "#723B13", + 900: "#633112", + }, + green: { + 50: "#F3FAF7", + 100: "#DEF7EC", + 200: "#BCF0DA", + 300: "#84E1BC", + 400: "#31C48D", + 500: "#0E9F6E", + 600: "#057A55", + 700: "#046C4E", + 800: "#03543F", + 900: "#014737", + }, + blue: { + 50: "#EBF5FF", + 100: "#E1EFFE", + 200: "#C3DDFD", + 300: "#A4CAFE", + 400: "#76A9FA", + 500: "#3F83F8", + 600: "#1C64F2", + 700: "#1A56DB", + 800: "#1E429F", + 900: "#233876", + }, + indigo: { + 50: "#F0F5FF", + 100: "#E5EDFF", + 200: "#CDDBFE", + 300: "#B4C6FC", + 400: "#8DA2FB", + 500: "#6875F5", + 600: "#5850EC", + 700: "#5145CD", + 800: "#42389D", + 900: "#362F78", + }, + purple: { + 50: "#F6F5FF", + 100: "#EDEBFE", + 200: "#DCD7FE", + 300: "#CABFFD", + 400: "#AC94FA", + 500: "#9061F9", + 600: "#7E3AF2", + 700: "#6C2BD9", + 800: "#5521B5", + 900: "#4A1D96", + }, + pink: { + 50: "#FDF2F8", + 100: "#FCE8F3", + 200: "#FAD1E8", + 300: "#F8B4D9", + 400: "#F17EB8", + 500: "#E74694", + 600: "#D61F69", + 700: "#BF125D", + 800: "#99154B", + 900: "#751A3D", + }, netbird: { DEFAULT: "#f68330", 50: "#fff6ed", diff --git a/client/ui-wails/main.go b/client/ui-wails/main.go index 2e351ec91..61d1a3c6b 100644 --- a/client/ui-wails/main.go +++ b/client/ui-wails/main.go @@ -102,8 +102,10 @@ func main() { window := app.Window.NewWithOptions(application.WebviewWindowOptions{ Title: "NetBird", - Width: 960, - Height: 640, + Width: 880, + Height: 620, + MinWidth: 880, + MinHeight: 620, Hidden: false, BackgroundColour: application.NewRGB(24, 26, 29), URL: "/",