Modernize the NETX login page with brand-first atmosphere.

Refresh typography, soft topology grid backdrop, and motion while keeping the force-change flow on the same shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-03 19:37:22 +08:00
parent 4c74d34d91
commit 4c256c521a
6 changed files with 252 additions and 33 deletions

View file

@ -4,6 +4,12 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Sora:wght@600;700&display=swap"
rel="stylesheet"
/>
<title>NETX</title>
</head>
<body>

View file

@ -440,7 +440,7 @@ const en = {
langEn: "English",
},
auth: {
loginTitle: "Sign in to NETX",
loginTitle: "Sign in",
username: "Username",
password: "Password",
login: "Sign in",

View file

@ -437,7 +437,7 @@ const zh = {
langEn: "English",
},
auth: {
loginTitle: "登录 NETX",
loginTitle: "登录",
username: "用户名",
password: "密码",
login: "登录",

View file

@ -6467,81 +6467,276 @@ pre {
}
.login-page {
--login-ink: #0f2744;
--login-accent: #0e7490;
--login-accent-deep: #155e75;
--login-panel: rgba(255, 255, 255, 0.88);
--login-line: rgba(15, 39, 68, 0.12);
--login-muted: #5b6b7c;
--login-text: #243447;
--login-danger: #b42318;
position: relative;
isolation: isolate;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
padding: 28px 20px;
overflow: hidden;
background:
radial-gradient(ellipse at 20% 20%, rgba(33, 150, 243, 0.18), transparent 55%),
radial-gradient(ellipse at 80% 0%, rgba(21, 101, 192, 0.2), transparent 45%),
#eef2f7;
linear-gradient(160deg, #e8eef5 0%, #f3f7fa 42%, #e4eef2 100%);
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
color: var(--login-text);
}
.login-page__atmosphere {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.login-page__orb {
position: absolute;
border-radius: 50%;
filter: blur(8px);
opacity: 0.55;
animation: login-orb-drift 18s ease-in-out infinite alternate;
}
.login-page__orb--a {
width: min(52vw, 520px);
height: min(52vw, 520px);
top: -12%;
left: -8%;
background: radial-gradient(circle at 35% 35%, #7dd3fc 0%, rgba(14, 116, 144, 0.25) 42%, transparent 70%);
}
.login-page__orb--b {
width: min(48vw, 460px);
height: min(48vw, 460px);
right: -10%;
bottom: -18%;
background: radial-gradient(circle at 40% 40%, #93c5fd 0%, rgba(15, 39, 68, 0.18) 48%, transparent 72%);
animation-delay: -6s;
animation-duration: 22s;
}
.login-page__grid {
position: absolute;
inset: -20%;
opacity: 0.28;
background-image:
linear-gradient(rgba(15, 39, 68, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(15, 39, 68, 0.06) 1px, transparent 1px);
background-size: 48px 48px;
mask-image: radial-gradient(ellipse at 50% 45%, #000 20%, transparent 72%);
animation: login-grid-pan 28s linear infinite;
}
.login-card {
position: relative;
z-index: 1;
width: min(400px, 100%);
padding: 28px 28px 24px;
background: #fff;
border: 1px solid #d8dee8;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
padding: 36px 32px 28px;
background: var(--login-panel);
border: 1px solid var(--login-line);
border-radius: 16px;
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
display: flex;
flex-direction: column;
gap: 12px;
gap: 14px;
animation: login-card-in 520ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.login-card__brand {
font-size: 22px;
font-family: Sora, "IBM Plex Sans", sans-serif;
font-size: clamp(2.4rem, 6vw, 3rem);
font-weight: 700;
color: #1565c0;
letter-spacing: 0.02em;
line-height: 1;
letter-spacing: 0.08em;
color: var(--login-ink);
animation: login-brand-in 700ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.login-card__title {
margin: 0;
font-size: 20px;
color: #0f172a;
margin: 0 0 4px;
font-family: Sora, "IBM Plex Sans", sans-serif;
font-size: 1.05rem;
font-weight: 600;
letter-spacing: 0.01em;
color: var(--login-muted);
}
.login-card__hint {
margin: 0;
color: #64748b;
margin: -4px 0 2px;
color: var(--login-muted);
font-size: 13px;
line-height: 1.45;
}
.login-card__label {
display: flex;
flex-direction: column;
gap: 6px;
gap: 7px;
font-size: 13px;
color: #334155;
font-weight: 500;
color: var(--login-ink);
}
.login-card__label input {
height: 36px;
padding: 0 10px;
height: 44px;
padding: 0 14px;
border: 1px solid var(--login-line);
border-radius: 10px;
background: rgba(255, 255, 255, 0.92);
color: var(--login-ink);
font: inherit;
font-size: 15px;
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
}
.login-card__label input:hover:not(:disabled) {
border-color: rgba(14, 116, 144, 0.35);
}
.login-card__label input:focus {
outline: none;
border-color: var(--login-accent);
box-shadow: 0 0 0 3px rgba(14, 116, 144, 0.18);
background: #fff;
}
.login-card__label input:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.login-card__error {
color: #b91c1c;
color: var(--login-danger);
font-size: 13px;
line-height: 1.4;
padding: 8px 10px;
border-radius: 8px;
background: rgba(180, 35, 24, 0.06);
border: 1px solid rgba(180, 35, 24, 0.14);
animation: login-error-in 220ms ease both;
}
.login-card__submit {
margin-top: 4px;
height: 38px;
margin-top: 6px;
height: 44px;
border: 0;
border-radius: 6px;
background: #1565c0;
border-radius: 10px;
background: linear-gradient(180deg, #0e8aa8 0%, var(--login-accent-deep) 100%);
color: #fff;
font-family: Sora, "IBM Plex Sans", sans-serif;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.02em;
cursor: pointer;
transition: transform 140ms ease, filter 140ms ease, opacity 140ms ease;
}
.login-card__submit:hover:not(:disabled) {
filter: brightness(1.05);
transform: translateY(-1px);
}
.login-card__submit:active:not(:disabled) {
transform: translateY(0);
filter: brightness(0.98);
}
.login-card__submit:disabled {
opacity: 0.6;
opacity: 0.55;
cursor: not-allowed;
}
.login-card__submit + .login-card__submit {
margin-top: 0;
background: #64748b;
}
@keyframes login-card-in {
from {
opacity: 0;
transform: translateY(18px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes login-brand-in {
from {
opacity: 0;
letter-spacing: 0.22em;
transform: translateY(8px);
}
to {
opacity: 1;
letter-spacing: 0.08em;
transform: translateY(0);
}
}
@keyframes login-orb-drift {
from {
transform: translate3d(0, 0, 0) scale(1);
}
to {
transform: translate3d(3%, 4%, 0) scale(1.06);
}
}
@keyframes login-grid-pan {
from {
transform: translate3d(0, 0, 0);
}
to {
transform: translate3d(-48px, -24px, 0);
}
}
@keyframes login-error-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.login-page__orb,
.login-page__grid,
.login-card,
.login-card__brand,
.login-card__error {
animation: none !important;
}
.login-card__submit:hover:not(:disabled) {
transform: none;
}
}
@media (max-width: 480px) {
.login-card {
padding: 28px 20px 22px;
}
.login-card__brand {
font-size: 2.2rem;
}
}
/* —— Network Management shell (left nav + main) ——
Classic ops palette aligned with brand header #0f2744:
cooler slate chrome, white content cards, blue active accent. */

View file

@ -43,8 +43,15 @@ export function ForceChangePasswordPage() {
return (
<div className="login-page">
<div className="login-page__atmosphere" aria-hidden="true">
<span className="login-page__orb login-page__orb--a" />
<span className="login-page__orb login-page__orb--b" />
<span className="login-page__grid" />
</div>
<form className="login-card" onSubmit={(e) => void onSubmit(e)}>
<div className="login-card__brand">NETX</div>
<div className="login-card__brand" aria-label="NETX">
NETX
</div>
<h1 className="login-card__title">{t("auth.forceChangeTitle")}</h1>
<p className="login-card__hint">
{t("auth.forceChangeHint", { user: user?.username || "admin" })}

View file

@ -32,8 +32,15 @@ export function LoginPage() {
return (
<div className="login-page">
<div className="login-page__atmosphere" aria-hidden="true">
<span className="login-page__orb login-page__orb--a" />
<span className="login-page__orb login-page__orb--b" />
<span className="login-page__grid" />
</div>
<form className="login-card" onSubmit={(e) => void onSubmit(e)}>
<div className="login-card__brand">NETX</div>
<div className="login-card__brand" aria-label="NETX">
NETX
</div>
<h1 className="login-card__title">{t("auth.loginTitle")}</h1>
<label className="login-card__label">
{t("auth.username")}
@ -55,7 +62,11 @@ export function LoginPage() {
disabled={busy || !ready}
/>
</label>
{error ? <div className="login-card__error">{error}</div> : null}
{error ? (
<div className="login-card__error" role="alert">
{error}
</div>
) : null}
<button type="submit" className="login-card__submit" disabled={busy || !ready || !password}>
{busy ? t("auth.loggingIn") : t("auth.login")}
</button>