web-ui(wave 2): migrate login.html to new primitives
Keeps the hero + AMPP Safe stamp + brand panel. Right column now uses wd-form-group / wd-input / wd-label / wd-btn primitives loaded from /dist/app.css. All JS ids and handlers preserved verbatim (login-form, setup-form, flash, show-setup, show-login). Visual changes: tighter form spacing, focus rings use accent-subtle ring, flash messages use the top-strip toast pattern.
This commit is contained in:
parent
75b94a5025
commit
16a34a2fad
1 changed files with 195 additions and 61 deletions
|
|
@ -5,55 +5,180 @@
|
||||||
<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/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<title>Sign in - Z-AMPP</title>
|
<title>Sign in - Z-AMPP</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="/dist/app.css">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
<style>
|
||||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
/* Page-only layout. Everything visual is from /dist/app.css primitives. */
|
||||||
:root{
|
body { display: grid; grid-template-columns: 1fr 460px; min-height: 100vh; margin: 0; }
|
||||||
--bg:#08090d;--surface:#11141b;--surface-2:#171a23;--border:#1f2330;
|
@media (max-width: 900px) {
|
||||||
--accent:#1f3ad0;--accent-strong:#3b50d6;--text:#e8eaf0;--text-dim:#7a8195;
|
body { grid-template-columns: 1fr; grid-template-rows: 40vh 1fr; }
|
||||||
--error:#e05555;--success:#3ecf6a;--radius:8px;--input-h:42px;
|
|
||||||
}
|
}
|
||||||
html,body{height:100%;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,sans-serif;font-size:14px;line-height:1.4;-webkit-font-smoothing:antialiased}
|
|
||||||
body{display:grid;grid-template-columns:1fr 460px;min-height:100vh}
|
|
||||||
@media (max-width: 900px){ body{grid-template-columns:1fr;grid-template-rows:40vh 1fr} }
|
|
||||||
|
|
||||||
/* === Hero === */
|
.hero {
|
||||||
.hero{position:relative;overflow:hidden;background:radial-gradient(ellipse at 30% 40%,#1a1d28 0%,#0a0b10 70%);display:flex;align-items:flex-end;padding:48px 56px}
|
position: relative;
|
||||||
.hero-img{position:absolute;inset:0;background-image:url(img/ampp-safe.png?v=hardhat3);background-size:contain;background-position:center;background-repeat:no-repeat}
|
overflow: hidden;
|
||||||
.hero-grad-bot{position:absolute;inset:auto 0 0 0;height:40%;background:linear-gradient(to top,rgba(8,9,13,.85),transparent);pointer-events:none}
|
background: radial-gradient(ellipse at 30% 40%, var(--bg-panel) 0%, var(--bg-deep) 70%);
|
||||||
.hero-stamp{position:absolute;top:32px;left:32px;display:flex;align-items:center;gap:10px;z-index:2;background:rgba(8,9,13,.55);backdrop-filter:blur(6px);padding:8px 14px 8px 12px;border:1px solid rgba(31,58,208,.25);border-radius:999px}
|
display: flex;
|
||||||
.hero-stamp-dot{width:8px;height:8px;background:var(--accent);border-radius:50%;box-shadow:0 0 12px var(--accent)}
|
align-items: flex-end;
|
||||||
.hero-stamp-text{font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--accent-strong)}
|
padding: 48px 56px;
|
||||||
.hero-caption{position:relative;z-index:2;max-width:520px}
|
}
|
||||||
.hero-caption h2{font-size:28px;font-weight:600;letter-spacing:-.01em;line-height:1.15;margin-bottom:10px}
|
.hero-img {
|
||||||
.hero-caption p{color:var(--text-dim);font-size:14px;line-height:1.55}
|
position: absolute;
|
||||||
.hero-caption .accent{color:var(--accent-strong)}
|
inset: 0;
|
||||||
|
background-image: url(img/ampp-safe.png?v=hardhat3);
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
.hero-grad-bot {
|
||||||
|
position: absolute;
|
||||||
|
inset: auto 0 0 0;
|
||||||
|
height: 40%;
|
||||||
|
background: linear-gradient(to top, var(--bg-deep) 0%, transparent 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.hero-stamp {
|
||||||
|
position: absolute;
|
||||||
|
top: 32px;
|
||||||
|
left: 32px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
background: var(--overlay);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid var(--accent-border);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.hero-stamp-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 10px var(--accent);
|
||||||
|
}
|
||||||
|
.hero-stamp-text {
|
||||||
|
font: 600 10px/1 var(--font);
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent-bright);
|
||||||
|
}
|
||||||
|
.hero-caption {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
max-width: 520px;
|
||||||
|
}
|
||||||
|
.hero-caption h2 {
|
||||||
|
font: 600 28px/1.15 var(--font);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
.hero-caption p {
|
||||||
|
font: 400 14px/1.55 var(--font);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.hero-caption .accent { color: var(--accent-bright); }
|
||||||
|
|
||||||
/* === Panel === */
|
/* Right column: brand + form panel */
|
||||||
.panel{display:flex;flex-direction:column;justify-content:center;padding:48px 56px;background:var(--surface);border-left:1px solid var(--border)}
|
.panel {
|
||||||
@media (max-width: 900px){ .panel{padding:32px 24px;border-left:none;border-top:1px solid var(--border)} }
|
display: flex;
|
||||||
.brand{display:flex;align-items:center;gap:10px;margin-bottom:36px}
|
flex-direction: column;
|
||||||
.brand-icon{width:32px;height:32px;background:var(--accent);color:#0a0b10;border-radius:7px;display:flex;align-items:center;justify-content:center;font-weight:700}
|
justify-content: center;
|
||||||
.brand-name{font-weight:600;font-size:15px;letter-spacing:-.01em}
|
padding: 48px 56px;
|
||||||
.brand-sub{font-size:10px;color:var(--text-dim);letter-spacing:.14em;text-transform:uppercase;margin-top:1px}
|
background: var(--bg-panel);
|
||||||
h1{font-size:22px;font-weight:600;letter-spacing:-.01em;margin-bottom:6px}
|
border-left: 1px solid var(--border);
|
||||||
.subtitle{color:var(--text-dim);font-size:13px;margin-bottom:28px}
|
}
|
||||||
.field{margin-bottom:16px}
|
@media (max-width: 900px) {
|
||||||
label{display:block;font-size:11px;font-weight:500;color:var(--text-dim);text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}
|
.panel { padding: 32px 24px; border-left: none; border-top: 1px solid var(--border); }
|
||||||
input{width:100%;height:var(--input-h);background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);font-size:14px;font-family:inherit;padding:0 14px;outline:none;transition:border-color .15s, background .15s}
|
}
|
||||||
input:focus{border-color:var(--accent);background:var(--bg)}
|
|
||||||
.btn{width:100%;height:42px;background:var(--accent);border:none;border-radius:var(--radius);color:#0a0b10;font-weight:600;font-size:14px;font-family:inherit;cursor:pointer;margin-top:10px;transition:background .15s}
|
.brand {
|
||||||
.btn:hover{background:var(--accent-strong)}
|
display: flex;
|
||||||
.btn:disabled{opacity:.5;cursor:not-allowed}
|
align-items: center;
|
||||||
.flash{border-radius:var(--radius);padding:10px 12px;font-size:13px;margin-bottom:18px;display:none}
|
gap: 10px;
|
||||||
.flash.error{background:rgba(224,85,85,.1);border:1px solid rgba(224,85,85,.4);color:var(--error);display:block}
|
margin-bottom: 36px;
|
||||||
.flash.success{background:rgba(62,207,106,.1);border:1px solid rgba(62,207,106,.4);color:var(--success);display:block}
|
}
|
||||||
.setup-link{text-align:center;margin-top:24px;font-size:12px;color:var(--text-dim)}
|
.brand-icon {
|
||||||
.setup-link a{color:var(--accent-strong);text-decoration:none}
|
width: 32px;
|
||||||
.setup-link a:hover{color:var(--accent);text-decoration:underline}
|
height: 32px;
|
||||||
#setup-panel{display:none}
|
background: var(--accent);
|
||||||
|
color: var(--bg-deep);
|
||||||
|
border-radius: 7px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font: 700 15px/1 var(--font);
|
||||||
|
}
|
||||||
|
.brand-name {
|
||||||
|
font: 600 15px/1.2 var(--font);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.brand-sub {
|
||||||
|
font: 500 10px/1 var(--font);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h1 {
|
||||||
|
font: 600 22px/1.2 var(--font);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
.panel .subtitle {
|
||||||
|
font: 400 13px/1.5 var(--font);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flash messages — inline toast-shaped */
|
||||||
|
.flash {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
font: 400 13px/1.4 var(--font);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.flash::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
.flash.error { display: block; border-color: var(--signal-bad); }
|
||||||
|
.flash.error::before { background: var(--signal-bad); }
|
||||||
|
.flash.success { display: block; border-color: var(--signal-good); }
|
||||||
|
.flash.success::before { background: var(--signal-good); }
|
||||||
|
|
||||||
|
.setup-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
font: 400 12px/1.4 var(--font);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
.setup-link a {
|
||||||
|
color: var(--accent-bright);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.setup-link a:hover { color: var(--accent); text-decoration: underline; }
|
||||||
|
|
||||||
|
#setup-panel { display: none; }
|
||||||
|
|
||||||
|
/* Make the login button full-width (the primitive is inline-flex by default) */
|
||||||
|
.wd-btn--full { width: 100%; margin-top: 10px; }
|
||||||
|
|
||||||
|
/* Stack form groups vertically with consistent gap */
|
||||||
|
.login-form { display: flex; flex-direction: column; gap: 14px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -84,28 +209,37 @@
|
||||||
<div id="login-panel">
|
<div id="login-panel">
|
||||||
<h1>Sign in</h1>
|
<h1>Sign in</h1>
|
||||||
<p class="subtitle">Enter your credentials to continue.</p>
|
<p class="subtitle">Enter your credentials to continue.</p>
|
||||||
<form id="login-form" autocomplete="on">
|
<form id="login-form" class="login-form" autocomplete="on">
|
||||||
<div class="field">
|
<div class="wd-form-group">
|
||||||
<label for="username">Username</label>
|
<label class="wd-label" for="username">Username</label>
|
||||||
<input id="username" name="username" type="text" autocomplete="username" required placeholder="username">
|
<input class="wd-input" id="username" name="username" type="text" autocomplete="username" required placeholder="username">
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="wd-form-group">
|
||||||
<label for="password">Password</label>
|
<label class="wd-label" for="password">Password</label>
|
||||||
<input id="password" name="password" type="password" autocomplete="current-password" required placeholder="••••••••">
|
<input class="wd-input" id="password" name="password" type="password" autocomplete="current-password" required placeholder="••••••••">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn" id="login-btn">Sign in</button>
|
<button type="submit" class="wd-btn wd-btn--primary wd-btn--md wd-btn--full" id="login-btn">Sign in</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="setup-link">First time? <a href="#" id="show-setup">Create admin account</a></div>
|
<div class="setup-link">First time? <a href="#" id="show-setup">Create admin account</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="setup-panel">
|
<div id="setup-panel">
|
||||||
<h1>Create admin</h1>
|
<h1>Create admin</h1>
|
||||||
<p class="subtitle">No accounts exist yet - create the first admin.</p>
|
<p class="subtitle">No accounts exist yet, create the first admin.</p>
|
||||||
<form id="setup-form" autocomplete="off">
|
<form id="setup-form" class="login-form" autocomplete="off">
|
||||||
<div class="field"><label for="su-username">Username</label><input id="su-username" name="username" type="text" required placeholder="admin"></div>
|
<div class="wd-form-group">
|
||||||
<div class="field"><label for="su-password">Password (min 8 chars)</label><input id="su-password" name="password" type="password" required minlength="8" placeholder="••••••••"></div>
|
<label class="wd-label" for="su-username">Username</label>
|
||||||
<div class="field"><label for="su-display">Display name (optional)</label><input id="su-display" name="display_name" type="text" placeholder="Admin"></div>
|
<input class="wd-input" id="su-username" name="username" type="text" required placeholder="admin">
|
||||||
<button type="submit" class="btn" id="setup-btn">Create account</button>
|
</div>
|
||||||
|
<div class="wd-form-group">
|
||||||
|
<label class="wd-label" for="su-password">Password (min 8 chars)</label>
|
||||||
|
<input class="wd-input" id="su-password" name="password" type="password" required minlength="8" placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
<div class="wd-form-group">
|
||||||
|
<label class="wd-label" for="su-display">Display name (optional)</label>
|
||||||
|
<input class="wd-input" id="su-display" name="display_name" type="text" placeholder="Admin">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="wd-btn wd-btn--primary wd-btn--md wd-btn--full" id="setup-btn">Create account</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="setup-link"><a href="#" id="show-login">Back to login</a></div>
|
<div class="setup-link"><a href="#" id="show-login">Back to login</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,7 +261,7 @@
|
||||||
try{
|
try{
|
||||||
const res = await fetch(API + '/login', {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',
|
const res = await fetch(API + '/login', {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',
|
||||||
body: JSON.stringify({username:$('username').value.trim(),password:$('password').value})});
|
body: JSON.stringify({username:$('username').value.trim(),password:$('password').value})});
|
||||||
if(res.ok){ showFlash('Signed in - redirecting...','success'); setTimeout(()=>{location.href='home.html'},600); }
|
if(res.ok){ showFlash('Signed in, redirecting...','success'); setTimeout(()=>{location.href='home.html'},600); }
|
||||||
else{ const d=await res.json().catch(()=>({})); showFlash(d.error||'Login failed','error'); }
|
else{ const d=await res.json().catch(()=>({})); showFlash(d.error||'Login failed','error'); }
|
||||||
} catch(err){ showFlash('Network error: '+err.message,'error'); }
|
} catch(err){ showFlash('Network error: '+err.message,'error'); }
|
||||||
finally{ btn.disabled=false; btn.textContent='Sign in'; }
|
finally{ btn.disabled=false; btn.textContent='Sign in'; }
|
||||||
|
|
@ -140,7 +274,7 @@
|
||||||
const res = await fetch(API + '/setup', {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',
|
const res = await fetch(API + '/setup', {method:'POST',headers:{'Content-Type':'application/json'},credentials:'same-origin',
|
||||||
body: JSON.stringify({username:$('su-username').value.trim(),password:$('su-password').value,display_name:$('su-display').value.trim()})});
|
body: JSON.stringify({username:$('su-username').value.trim(),password:$('su-password').value,display_name:$('su-display').value.trim()})});
|
||||||
if(res.ok){
|
if(res.ok){
|
||||||
showFlash('Admin account created - you can now log in','success');
|
showFlash('Admin account created, you can now log in','success');
|
||||||
setTimeout(()=>{ $('setup-panel').style.display='none'; $('login-panel').style.display='block'; },1200);
|
setTimeout(()=>{ $('setup-panel').style.display='none'; $('login-panel').style.display='block'; },1200);
|
||||||
} else{
|
} else{
|
||||||
const d=await res.json().catch(()=>({}));
|
const d=await res.json().catch(()=>({}));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue