feat: add branding assets and favicon support across admin-web and desktop-client
Add NexaVPN logo images (full logo and mark-only variants) to admin-web and desktop-client public directories. Add favicon.ico and favicon.png to admin-web, and icon.png to desktop-client. Update index.html files to reference favicon assets. Add icon.png and icon.ico to desktop-client Tauri icons directory and configure bundle.icon in tauri.conf.json. Update Layout component to display logo in sidebar brand-block with
@@ -3,6 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/png" href="/NexaVPN_Logo_Only.png" />
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<title>NexaVPN Admin</title>
|
<title>NexaVPN Admin</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
admin-web/public/NexaVPN_Logo.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
admin-web/public/NexaVPN_Logo_Only.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
admin-web/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
admin-web/public/favicon.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
@@ -14,9 +14,13 @@ export function Layout() {
|
|||||||
return (
|
return (
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
<div>
|
<div className="brand-block">
|
||||||
<p className="eyebrow">NexaVPN</p>
|
<img className="brand-logo brand-logo-full" src="/NexaVPN_Logo.png" alt="NexaVPN" />
|
||||||
<h1>Control Plane</h1>
|
<div className="brand-copy">
|
||||||
|
<p className="eyebrow">NexaVPN</p>
|
||||||
|
<h1>Control Plane</h1>
|
||||||
|
<p className="brand-tagline">Remote access orchestration for your private WireGuard edge.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav className="nav">
|
<nav className="nav">
|
||||||
{items.map(([label, path]) => (
|
{items.map(([label, path]) => (
|
||||||
@@ -32,9 +36,12 @@ export function Layout() {
|
|||||||
</aside>
|
</aside>
|
||||||
<main className="content">
|
<main className="content">
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div>
|
<div className="topbar-brand">
|
||||||
<p className="eyebrow">Enterprise WireGuard</p>
|
<img className="brand-logo brand-logo-mark" src="/NexaVPN_Logo_Only.png" alt="NexaVPN mark" />
|
||||||
<h2>Self-hosted VPN management</h2>
|
<div>
|
||||||
|
<p className="eyebrow">Enterprise WireGuard</p>
|
||||||
|
<h2>Self-hosted VPN management</h2>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pill">Secure by design</div>
|
<div className="pill">Secure by design</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -58,8 +58,13 @@ export function LoginPage({ onAuthenticated }: LoginPageProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="auth-shell">
|
<div className="auth-shell">
|
||||||
<form className="auth-card" onSubmit={onSubmit}>
|
<form className="auth-card" onSubmit={onSubmit}>
|
||||||
<p className="eyebrow">NexaVPN Admin</p>
|
<div className="auth-brand">
|
||||||
<h2>{mode === "login" ? "Sign in" : "Create initial admin"}</h2>
|
<img className="brand-logo brand-logo-full" src="/NexaVPN_Logo.png" alt="NexaVPN" />
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">NexaVPN Admin</p>
|
||||||
|
<h2>{mode === "login" ? "Sign in" : "Create initial admin"}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p className="auth-copy">
|
<p className="auth-copy">
|
||||||
{mode === "login"
|
{mode === "login"
|
||||||
? "Use your NexaVPN admin credentials."
|
? "Use your NexaVPN admin credentials."
|
||||||
|
|||||||
@@ -61,6 +61,45 @@ button {
|
|||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-brand,
|
||||||
|
.brand-block,
|
||||||
|
.topbar-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-block {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-tagline {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 240px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo-full {
|
||||||
|
width: min(100%, 220px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo-mark {
|
||||||
|
width: 48px;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.auth-card label {
|
.auth-card label {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -258,6 +297,18 @@ button {
|
|||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auth-brand,
|
||||||
|
.brand-block,
|
||||||
|
.topbar-brand,
|
||||||
|
.page-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo-full {
|
||||||
|
width: min(100%, 200px);
|
||||||
|
}
|
||||||
|
|
||||||
.grid.two,
|
.grid.two,
|
||||||
.grid.three {
|
.grid.three {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/png" href="/icon.png" />
|
||||||
<title>NexaVPN</title>
|
<title>NexaVPN</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
desktop-client/public/icon.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
desktop-client/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
desktop-client/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
@@ -25,7 +25,10 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"icon": [],
|
"icon": [
|
||||||
|
"icons/icon.png",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
"resources": [
|
"resources": [
|
||||||
"bundled/**/*"
|
"bundled/**/*"
|
||||||
]
|
]
|
||||||
|
|||||||
50
scripts/generate_ico_from_png.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("usage: generate_ico_from_png.py <input.png> <output.ico>", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
source = Path(sys.argv[1])
|
||||||
|
target = Path(sys.argv[2])
|
||||||
|
|
||||||
|
png = source.read_bytes()
|
||||||
|
if not png.startswith(PNG_SIGNATURE):
|
||||||
|
print(f"{source} is not a PNG file", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
width = int.from_bytes(png[16:20], "big")
|
||||||
|
height = int.from_bytes(png[20:24], "big")
|
||||||
|
width_byte = 0 if width >= 256 else width
|
||||||
|
height_byte = 0 if height >= 256 else height
|
||||||
|
|
||||||
|
header = struct.pack("<HHH", 0, 1, 1)
|
||||||
|
directory = struct.pack(
|
||||||
|
"<BBBBHHII",
|
||||||
|
width_byte,
|
||||||
|
height_byte,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
32,
|
||||||
|
len(png),
|
||||||
|
len(header) + 16,
|
||||||
|
)
|
||||||
|
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_bytes(header + directory + png)
|
||||||
|
print(f"wrote {target}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||