From 1ddcbf0b141271cb3b187581a675a2b403691bf0 Mon Sep 17 00:00:00 2001 From: nessi Date: Wed, 18 Mar 2026 11:42:34 +0100 Subject: [PATCH] refactor: extract UI components and redesign desktop client interface with improved visual hierarchy Extract App.tsx logic into reusable components: AppHeader, ResourcePanel, StatusCard, StatTile, and ActionButton. Replace inline markup with component composition and props-based data flow. Redesign visual system with enhanced gradients, refined color palette, and improved spacing. Update app-shell grid layout with 18px gaps and 1140px max width. Add radial gradient overlays and linear background --- desktop-client/src/App.tsx | 122 +--- .../src/components/ActionButton.tsx | 28 + desktop-client/src/components/AppHeader.tsx | 56 ++ .../src/components/ResourcePanel.tsx | 49 ++ desktop-client/src/components/StatTile.tsx | 17 + desktop-client/src/components/StatusCard.tsx | 86 +++ desktop-client/src/styles.css | 588 ++++++++++++------ 7 files changed, 681 insertions(+), 265 deletions(-) create mode 100644 desktop-client/src/components/ActionButton.tsx create mode 100644 desktop-client/src/components/AppHeader.tsx create mode 100644 desktop-client/src/components/ResourcePanel.tsx create mode 100644 desktop-client/src/components/StatTile.tsx create mode 100644 desktop-client/src/components/StatusCard.tsx diff --git a/desktop-client/src/App.tsx b/desktop-client/src/App.tsx index 9e7f31e..2ec8560 100644 --- a/desktop-client/src/App.tsx +++ b/desktop-client/src/App.tsx @@ -1,5 +1,8 @@ import { FormEvent, useEffect, useMemo, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; +import { AppHeader } from "./components/AppHeader"; +import { ResourcePanel } from "./components/ResourcePanel"; +import { StatusCard } from "./components/StatusCard"; type EnrollmentState = { assignedIp: string; @@ -191,40 +194,23 @@ export function App() { return (
-
-
-
- NexaVPN -
-

NexaVPN

-

VPN Client

-

{state ? "Private access client" : "Sign in to add this device"}

-
-
-
- {state ? ( - - ) : null} - {state ? ( - - ) : null} - -
-
+
+
{!state ? ( -
-
+
+
-

Sign in

-

Provision device

+

Sign in

+

Provision device

@@ -241,75 +227,31 @@ export function App() { setPassword(event.target.value)} /> {error ?
{error}
: null} -
-
) : ( -
-
-
-

Status

-

{connected ? "Connected" : "Disconnected"}

-
- - {connected ? "Tunnel active" : "Ready"} -
-
-
- -
-
-
-

Connection

-

Overview

-
-
-
-
- Assigned VPN IP - {state.assignedIp} -
-
- Gateway endpoint - {state.gatewayEndpoint} -
-
- Access - {profileLabel} -
-
- Last sync - {state.lastSyncTime} -
-
-
- +
+ {error ?
{error}
: null} -
+
)} - +
diff --git a/desktop-client/src/components/ActionButton.tsx b/desktop-client/src/components/ActionButton.tsx new file mode 100644 index 0000000..c5cf705 --- /dev/null +++ b/desktop-client/src/components/ActionButton.tsx @@ -0,0 +1,28 @@ +import type { ReactNode } from "react"; + +type ActionButtonProps = { + children: ReactNode; + onClick?: () => void; + type?: "button" | "submit"; + disabled?: boolean; + variant?: "primary" | "secondary" | "ghost" | "danger"; +}; + +export function ActionButton({ + children, + onClick, + type = "button", + disabled = false, + variant = "secondary" +}: ActionButtonProps) { + return ( + + ); +} diff --git a/desktop-client/src/components/AppHeader.tsx b/desktop-client/src/components/AppHeader.tsx new file mode 100644 index 0000000..3dbd1f4 --- /dev/null +++ b/desktop-client/src/components/AppHeader.tsx @@ -0,0 +1,56 @@ +import { ActionButton } from "./ActionButton"; + +type AppHeaderProps = { + enrolled: boolean; + connected: boolean; + syncing: boolean; + onSync: () => void; + onLogout: () => void; + onToggleConnection: () => void; +}; + +export function AppHeader({ + enrolled, + connected, + syncing, + onSync, + onLogout, + onToggleConnection +}: AppHeaderProps) { + return ( +
+
+
+ NexaVPN +
+
+

NexaVPN

+

Secure Access Client

+

+ Enterprise-grade private network access with policy-based resources. +

+
+
+ +
+ {enrolled ? ( + <> + + {syncing ? "Syncing..." : "Sync"} + + + Logout + + + ) : null} + + {!enrolled ? "Provision first" : connected ? "Disconnect" : "Connect"} + +
+
+ ); +} diff --git a/desktop-client/src/components/ResourcePanel.tsx b/desktop-client/src/components/ResourcePanel.tsx new file mode 100644 index 0000000..97e1884 --- /dev/null +++ b/desktop-client/src/components/ResourcePanel.tsx @@ -0,0 +1,49 @@ +import { ActionButton } from "./ActionButton"; + +function ResourceListItem({ value }: { value: string }) { + return ( +
  • + + {value} +
  • + ); +} + +type ResourcePanelProps = { + resources: string[]; + profileLabel: string; + onReset: () => void; +}; + +export function ResourcePanel({ resources, profileLabel, onReset }: ResourcePanelProps) { + const effectiveResources = resources.length > 0 ? resources : ["Keine Ressourcen zugewiesen"]; + + return ( + + ); +} diff --git a/desktop-client/src/components/StatTile.tsx b/desktop-client/src/components/StatTile.tsx new file mode 100644 index 0000000..c7d91d4 --- /dev/null +++ b/desktop-client/src/components/StatTile.tsx @@ -0,0 +1,17 @@ +type StatTileProps = { + label: string; + value: string; + icon: React.ReactNode; +}; + +export function StatTile({ label, value, icon }: StatTileProps) { + return ( +
    +
    {icon}
    +
    + {label} + {value} +
    +
    + ); +} diff --git a/desktop-client/src/components/StatusCard.tsx b/desktop-client/src/components/StatusCard.tsx new file mode 100644 index 0000000..2c1c7ff --- /dev/null +++ b/desktop-client/src/components/StatusCard.tsx @@ -0,0 +1,86 @@ +import { StatTile } from "./StatTile"; + +function DotGridIcon() { + return ( + + ); +} + +function ShieldIcon() { + return ( + + ); +} + +function NetworkIcon() { + return ( + + ); +} + +function ClockIcon() { + return ( + + ); +} + +type StatusCardProps = { + connected: boolean; + assignedIp: string; + gatewayEndpoint: string; + accessLabel: string; + lastSyncTime: string; +}; + +export function StatusCard({ + connected, + assignedIp, + gatewayEndpoint, + accessLabel, + lastSyncTime +}: StatusCardProps) { + return ( +
    +
    +
    +

    Status

    +

    {connected ? "Connected" : "Disconnected"}

    +

    + {connected ? "Secure tunnel established" : "Client is ready to establish a secure tunnel"} +

    +
    +
    + + {connected ? "Tunnel active" : "Ready"} +
    +
    + +
    +
    +
    +

    Connection

    +

    Overview

    +
    +
    + +
    + } label="Assigned VPN IP" value={assignedIp} /> + } label="Gateway endpoint" value={gatewayEndpoint} /> + } label="Access mode" value={accessLabel} /> + } label="Last sync" value={lastSyncTime} /> +
    +
    +
    + ); +} diff --git a/desktop-client/src/styles.css b/desktop-client/src/styles.css index 59b873d..7b51530 100644 --- a/desktop-client/src/styles.css +++ b/desktop-client/src/styles.css @@ -1,10 +1,10 @@ :root { - font-family: "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif; - color: #eef4ff; + font-family: "Segoe UI Variable", "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif; + color: #eef6ff; background: - radial-gradient(circle at 12% 10%, rgba(50, 196, 167, 0.2), transparent 24%), - radial-gradient(circle at 90% 18%, rgba(89, 133, 255, 0.16), transparent 22%), - linear-gradient(180deg, #07101c 0%, #0c1524 100%); + radial-gradient(circle at 12% 12%, rgba(45, 204, 188, 0.18), transparent 24%), + radial-gradient(circle at 88% 16%, rgba(64, 116, 255, 0.16), transparent 22%), + linear-gradient(180deg, #06101d 0%, #0a1423 52%, #0c1523 100%); } * { @@ -14,7 +14,8 @@ html, body, #root { - height: 100vh; + width: 100%; + height: 100%; } body { @@ -28,292 +29,529 @@ input { } .client-shell { - height: 100vh; - padding: 12px; + width: 100%; + height: 100%; + padding: 18px; overflow: hidden; } -.app-frame { - width: min(920px, 100%); - height: calc(100vh - 24px); +.app-shell { + width: min(1140px, 100%); + height: 100%; margin: 0 auto; display: grid; grid-template-rows: auto 1fr; - gap: 10px; - overflow: hidden; + gap: 18px; } -.top-strip { +.app-header, +.status-card, +.resource-panel, +.login-card, +.error { + background: linear-gradient(180deg, rgba(14, 24, 40, 0.88) 0%, rgba(11, 19, 33, 0.9) 100%); + border: 1px solid rgba(142, 174, 226, 0.13); + box-shadow: 0 28px 80px rgba(2, 8, 18, 0.34); +} + +.app-header { + border-radius: 28px; + padding: 22px 24px; display: flex; align-items: center; justify-content: space-between; - gap: 12px; + gap: 24px; + min-height: 112px; } -.brand-lockup { +.brand-block { + display: flex; + align-items: center; + gap: 18px; + min-width: 0; +} + +.brand-icon-shell { + width: 64px; + height: 64px; + border-radius: 20px; + display: grid; + place-items: center; + background: linear-gradient(180deg, rgba(17, 45, 64, 0.92) 0%, rgba(11, 24, 36, 0.88) 100%); + border: 1px solid rgba(108, 211, 200, 0.14); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +.brand-icon { + width: 48px; + height: 48px; +} + +.brand-text { + min-width: 0; + display: grid; + gap: 4px; +} + +.section-eyebrow { + margin: 0; + color: #73dfc0; + letter-spacing: 0.18em; + text-transform: uppercase; + font-size: 0.72rem; + font-weight: 700; +} + +.brand-text h1, +.panel-head h2, +.status-hero h2, +.panel-head h3 { + margin: 0; +} + +.brand-text h1 { + font-size: 2rem; + line-height: 1.05; + letter-spacing: -0.04em; +} + +.brand-subtitle { + margin: 0; + color: #9fb3d4; + font-size: 0.98rem; + max-width: 620px; +} + +.header-actions { display: flex; align-items: center; gap: 10px; -} - -.brand-lockup img { - width: 44px; - height: 44px; - border-radius: 12px; - box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28); -} - -.brand-copy { - display: grid; - gap: 2px; -} - -.eyebrow { - margin: 0; - color: #75e3ba; - letter-spacing: 0.18em; - text-transform: uppercase; - font-size: 0.68rem; -} - -.brand-copy h1, -.status-panel h3 { - margin: 0; - line-height: 1.1; -} - -.brand-copy h1 { - font-size: 2rem; -} - -.brand-copy p { - font-size: 0.98rem; -} - -.brand-copy p, -.hero-copy p, -.status-panel p, -.surface-header p, -.detail-card span, -.profile-card span { - margin: 0; - color: #9eb1d1; -} - -.top-actions { - display: flex; - gap: 8px; flex-wrap: wrap; } -.shell-button, -.shell-button-secondary { - border: 0; +.action-button { + border: 1px solid transparent; border-radius: 999px; - padding: 9px 15px; + min-height: 44px; + padding: 0 18px; font-weight: 700; cursor: pointer; - transition: 160ms ease; + transition: + background-color 180ms ease, + border-color 180ms ease, + transform 180ms ease, + box-shadow 180ms ease, + opacity 180ms ease; } -.shell-button { - background: linear-gradient(135deg, #74e0b8 0%, #1fb67a 100%); - color: #04131a; +.action-button:hover:not(:disabled) { + transform: translateY(-1px); } -.shell-button-secondary { - background: rgba(255, 255, 255, 0.04); - color: #eef4ff; - border: 1px solid rgba(177, 197, 229, 0.16); +.action-button:active:not(:disabled) { + transform: translateY(0); } -.shell-button:disabled, -.shell-button-secondary:disabled { - opacity: 0.58; +.action-button:disabled { + opacity: 0.55; cursor: default; } -.surface, -.status-panel, -.login-panel { - background: rgba(11, 20, 35, 0.78); - border: 1px solid rgba(177, 197, 229, 0.12); - box-shadow: 0 24px 70px rgba(2, 8, 18, 0.32); - backdrop-filter: blur(18px); +.action-button-primary { + color: #04131a; + background: linear-gradient(135deg, #73e0c7 0%, #30b89a 100%); + box-shadow: 0 12px 28px rgba(43, 188, 151, 0.18); +} + +.action-button-secondary { + color: #eef6ff; + background: rgba(255, 255, 255, 0.04); + border-color: rgba(176, 198, 228, 0.14); +} + +.action-button-secondary:hover:not(:disabled), +.action-button-ghost:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.07); + border-color: rgba(176, 198, 228, 0.22); +} + +.action-button-ghost { + color: #c0d1ea; + background: transparent; + border-color: rgba(176, 198, 228, 0.08); +} + +.action-button-danger { + color: #f8fbff; + background: linear-gradient(135deg, #1f4a54 0%, #107a6e 100%); + border-color: rgba(117, 225, 204, 0.22); + box-shadow: 0 10px 28px rgba(17, 137, 121, 0.18); } .body-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) 250px; - gap: 12px; min-height: 0; - overflow: hidden; + display: grid; + grid-template-columns: minmax(0, 1fr) 290px; + gap: 18px; align-items: start; } -.login-panel, -.status-panel { - border-radius: 28px; - padding: 14px; - display: grid; - gap: 10px; +.main-column { min-height: 0; - overflow: hidden; + display: grid; + grid-template-rows: auto auto; + gap: 14px; } -.status-panel > p { - margin: 0; - color: #9eb1d1; - line-height: 1.5; +.status-card { + min-height: 0; + border-radius: 30px; + padding: 22px; + display: grid; + gap: 18px; } -.status-top { +.status-hero { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; - gap: 10px; + gap: 16px; } -.status-state { +.status-hero h2 { + font-size: 2rem; + line-height: 1.02; + letter-spacing: -0.04em; +} + +.status-support { + margin: 8px 0 0; + color: #9db3d6; + font-size: 0.98rem; +} + +.status-pill { display: inline-flex; align-items: center; - gap: 8px; - padding: 6px 10px; + gap: 10px; + padding: 10px 14px; border-radius: 999px; - background: rgba(255, 255, 255, 0.04); - color: #c8d6ee; + background: rgba(255, 255, 255, 0.045); + color: #d7e3f5; + border: 1px solid rgba(176, 198, 228, 0.1); + white-space: nowrap; } -.status-dot { +.status-pill.is-online { + background: rgba(46, 188, 156, 0.12); + border-color: rgba(111, 226, 194, 0.16); +} + +.status-pulse { width: 10px; height: 10px; border-radius: 999px; - background: #ff8a7d; + background: #ff8e84; + box-shadow: 0 0 0 0 rgba(255, 142, 132, 0.34); } -.status-dot.online { - background: #74e0b8; +.status-pill.is-online .status-pulse { + background: #73dfc0; + animation: pulse 1.8s infinite; } -.surface { - border-radius: 24px; - padding: 12px; +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(115, 223, 192, 0.36); + } + 70% { + box-shadow: 0 0 0 10px rgba(115, 223, 192, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(115, 223, 192, 0); + } +} + +.status-details { + min-height: 0; display: grid; - gap: 10px; + gap: 14px; + padding: 16px; + border-radius: 24px; + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(154, 181, 228, 0.08); } -.surface-header { +.panel-head { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 12px; } -.surface-header h4 { - margin: 0; - font-size: 0.95rem; +.panel-head h2 { + font-size: 1.3rem; + line-height: 1.1; } -.status-grid, -.profile-grid { +.panel-head h3 { + font-size: 1rem; + line-height: 1.1; +} + +.panel-head-compact { + align-items: center; +} + +.stat-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; - align-content: start; + gap: 12px; } -.detail-card, -.profile-card { - padding: 10px 12px; - border-radius: 16px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(177, 197, 229, 0.1); +.stat-tile { + min-height: 86px; + padding: 14px; + border-radius: 20px; display: grid; - gap: 4px; - min-height: 72px; + grid-template-columns: 34px 1fr; + gap: 12px; + align-items: start; + background: rgba(255, 255, 255, 0.035); + border: 1px solid rgba(154, 181, 228, 0.08); + transition: + border-color 180ms ease, + background-color 180ms ease, + transform 180ms ease; } -.detail-card strong, -.profile-card strong { - font-size: 0.92rem; - word-break: break-word; +.stat-tile:hover { + transform: translateY(-1px); + border-color: rgba(118, 218, 200, 0.16); + background: rgba(255, 255, 255, 0.05); } -.detail-card span { - font-size: 0.9rem; +.stat-tile-icon { + width: 34px; + height: 34px; + border-radius: 12px; + display: grid; + place-items: center; + background: linear-gradient(180deg, rgba(30, 71, 88, 0.75) 0%, rgba(17, 31, 48, 0.72) 100%); + border: 1px solid rgba(111, 226, 194, 0.12); } -.resource-stack { - margin: 0; - padding: 0; - list-style: none; +.stat-tile-icon svg { + width: 16px; + height: 16px; + fill: none; + stroke: #80e4cc; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.stat-tile-copy { + min-width: 0; display: grid; gap: 6px; - align-content: start; } -.resource-stack li { - padding: 10px 12px; - border-radius: 14px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(177, 197, 229, 0.1); - color: #eef4ff; +.stat-tile-copy span { + color: #8fa6ca; + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.stat-tile-copy strong { + color: #f4f8ff; + font-size: 1.02rem; + line-height: 1.25; + letter-spacing: -0.015em; word-break: break-word; - min-height: 44px; - display: flex; - align-items: center; - font-size: 0.94rem; } -.login-panel form { +.resource-panel, +.login-card { + min-height: 0; + border-radius: 30px; + padding: 20px; display: grid; gap: 16px; } -.login-panel label { +.resource-panel { + grid-template-rows: auto auto 1fr auto; +} + +.resource-count { + min-width: 34px; + height: 34px; + padding: 0 10px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + color: #80e4cc; + background: rgba(23, 62, 71, 0.8); + border: 1px solid rgba(111, 226, 194, 0.12); +} + +.resource-meta { + display: grid; + gap: 4px; + padding: 12px 14px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(154, 181, 228, 0.08); +} + +.resource-meta-label { + color: #8fa6ca; + font-size: 0.75rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.resource-list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + align-content: start; + gap: 10px; + min-height: 0; +} + +.resource-item { + min-height: 54px; + padding: 12px 14px; + border-radius: 18px; + display: flex; + align-items: center; + gap: 10px; + background: rgba(255, 255, 255, 0.035); + border: 1px solid rgba(154, 181, 228, 0.08); +} + +.resource-item-dot { + width: 9px; + height: 9px; + border-radius: 999px; + background: #73dfc0; + box-shadow: 0 0 0 4px rgba(115, 223, 192, 0.08); + flex: none; +} + +.resource-item-text { + font-weight: 600; + color: #eff6ff; + word-break: break-word; +} + +.resource-footer { + display: flex; +} + +.resource-footer .action-button { + width: 100%; +} + +.login-card form { + display: grid; + gap: 16px; +} + +.login-card label { display: grid; gap: 8px; color: #c2cfe5; } -.login-panel input { +.login-card input { border: 1px solid rgba(177, 197, 229, 0.16); background: rgba(7, 14, 27, 0.9); color: #f5f7fb; border-radius: 16px; - padding: 15px 16px; + padding: 14px 16px; } -.login-card-actions { +.login-actions { display: flex; - gap: 12px; - flex-wrap: wrap; } .error { - padding: 10px 12px; - border-radius: 16px; + padding: 12px 14px; + border-radius: 18px; background: rgba(255, 115, 115, 0.08); - border: 1px solid rgba(255, 115, 115, 0.16); + border-color: rgba(255, 115, 115, 0.15); color: #ffc3c3; white-space: pre-wrap; - font-size: 0.95rem; +} + +@media (max-width: 980px) { + .client-shell { + padding: 14px; + } + + .app-shell { + width: 100%; + gap: 14px; + } + + .app-header { + padding: 18px; + } + + .brand-text h1 { + font-size: 1.8rem; + } + + .body-grid { + grid-template-columns: minmax(0, 1fr) 260px; + gap: 14px; + } } @media (max-width: 760px) { - .body-grid { - grid-template-columns: 1fr; + body { + overflow: auto; } - .top-strip, - .status-top, - .surface-header { - align-items: flex-start; + .client-shell { + height: auto; + min-height: 100%; + overflow: visible; + } + + .app-shell { + height: auto; + grid-template-rows: auto; + } + + .app-header, + .status-hero, + .panel-head { flex-direction: column; + align-items: flex-start; } - .status-grid, - .profile-grid { + .header-actions, + .resource-footer, + .login-actions { + width: 100%; + } + + .header-actions .action-button, + .resource-footer .action-button, + .login-actions .action-button { + width: 100%; + } + + .body-grid, + .stat-grid { grid-template-columns: 1fr; } }