@@ -1165,35 +1165,39 @@ sub app_html {
|
||
| 1165 | 1165 |
--login-form-width: calc((var(--otp-size) * 6) + (var(--otp-gap) * 5)); |
| 1166 | 1166 |
background: #fff; |
| 1167 | 1167 |
border-radius: 16px; |
| 1168 |
- padding: 48px 36px 44px; |
|
| 1168 |
+ padding: 56px; |
|
| 1169 | 1169 |
width: 100%; |
| 1170 |
- max-width: 424px; |
|
| 1170 |
+ max-width: 760px; |
|
| 1171 |
+ min-height: 420px; |
|
| 1171 | 1172 |
display: grid; |
| 1172 |
- gap: 26px; |
|
| 1173 |
+ grid-template-columns: minmax(220px, 1fr) minmax(0, var(--login-form-width)); |
|
| 1174 |
+ align-items: center; |
|
| 1175 |
+ gap: 56px; |
|
| 1173 | 1176 |
box-shadow: 0 8px 40px rgba(0,0,0,.28); |
| 1174 | 1177 |
} |
| 1175 |
- .login-card .brand { text-align: center; display: grid; gap: 6px; }
|
|
| 1178 |
+ .login-card .brand { text-align: left; display: grid; gap: 8px; }
|
|
| 1176 | 1179 |
.login-card .brand .icon {
|
| 1177 |
- margin: 0 auto 4px; |
|
| 1178 |
- width: 52px; height: 52px; border-radius: 14px; |
|
| 1180 |
+ margin: 0 0 12px; |
|
| 1181 |
+ width: 64px; height: 64px; border-radius: 18px; |
|
| 1179 | 1182 |
background: #e8f0fe; display: flex; align-items: center; justify-content: center; |
| 1180 | 1183 |
} |
| 1181 |
- .login-card .brand .icon svg { width: 26px; height: 26px; fill: var(--accent); }
|
|
| 1182 |
- .login-card .brand h1 { margin: 0; font-size: 22px; font-weight: 700; color: var(--ink); }
|
|
| 1183 |
- .login-card .brand p { margin: 0; color: var(--muted); font-size: 13px; }
|
|
| 1184 |
+ .login-card .brand .icon svg { width: 38px; height: 38px; fill: none; stroke: var(--accent); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
|
|
| 1185 |
+ .login-card .brand h1 { margin: 0; font-size: 32px; line-height: 1.05; font-weight: 750; color: var(--ink); }
|
|
| 1186 |
+ .login-card .brand p { margin: 0; color: var(--muted); font-size: 16px; }
|
|
| 1184 | 1187 |
.login-card form {
|
| 1185 | 1188 |
display: grid; |
| 1186 | 1189 |
gap: 16px; |
| 1187 | 1190 |
width: min(100%, var(--login-form-width)); |
| 1188 |
- justify-self: center; |
|
| 1191 |
+ justify-self: end; |
|
| 1192 |
+ padding-bottom: clamp(92px, 12vh, 132px); |
|
| 1189 | 1193 |
} |
| 1194 |
+ .login-card form.busy { opacity: .72; pointer-events: none; }
|
|
| 1190 | 1195 |
.login-card .field-label { font-size: 13px; font-weight: 600; color: var(--ink); }
|
| 1191 | 1196 |
/* 6 separate OTP digit boxes */ |
| 1192 | 1197 |
.otp-row {
|
| 1193 | 1198 |
display: flex; |
| 1194 | 1199 |
gap: var(--otp-gap); |
| 1195 | 1200 |
justify-content: center; |
| 1196 |
- margin-bottom: clamp(92px, 13vh, 132px); |
|
| 1197 | 1201 |
} |
| 1198 | 1202 |
.otp-row input {
|
| 1199 | 1203 |
width: var(--otp-size); height: 56px; border: 1.5px solid #dde2ec; border-radius: 10px; |
@@ -1203,17 +1207,24 @@ sub app_html {
|
||
| 1203 | 1207 |
} |
| 1204 | 1208 |
.otp-row input:focus { border-color: var(--accent); background: #fff; }
|
| 1205 | 1209 |
.otp-row input.filled { border-color: #b3c6f0; background: #fff; }
|
| 1206 |
- .login-card button.primary {
|
|
| 1207 |
- width: 100%; border: none; background: var(--accent); color: #fff; |
|
| 1208 |
- border-radius: 10px; padding: 13px; font: inherit; font-size: 15px; |
|
| 1209 |
- font-weight: 600; cursor: pointer; min-height: 48px; |
|
| 1210 |
- display: flex; align-items: center; justify-content: center; gap: 8px; |
|
| 1211 |
- } |
|
| 1212 |
- .login-card button.primary:hover { background: #0f52b8; }
|
|
| 1213 |
- .login-card button.primary:disabled { opacity: .55; cursor: not-allowed; }
|
|
| 1214 | 1210 |
#login-error {
|
| 1215 | 1211 |
color: var(--bad); font-size: 13px; text-align: center; |
| 1216 |
- min-height: 18px; margin-top: -8px; |
|
| 1212 |
+ min-height: 18px; margin-top: -88px; grid-column: 2; |
|
| 1213 |
+ } |
|
| 1214 |
+ @media (max-width: 760px) {
|
|
| 1215 |
+ .login-card {
|
|
| 1216 |
+ max-width: 424px; |
|
| 1217 |
+ min-height: 0; |
|
| 1218 |
+ padding: 48px 36px 44px; |
|
| 1219 |
+ grid-template-columns: 1fr; |
|
| 1220 |
+ gap: 26px; |
|
| 1221 |
+ } |
|
| 1222 |
+ .login-card .brand { text-align: center; }
|
|
| 1223 |
+ .login-card .brand .icon { margin: 0 auto 8px; }
|
|
| 1224 |
+ .login-card .brand h1 { font-size: 24px; }
|
|
| 1225 |
+ .login-card .brand p { font-size: 14px; }
|
|
| 1226 |
+ .login-card form { justify-self: center; padding-bottom: 96px; }
|
|
| 1227 |
+ #login-error { grid-column: 1; margin-top: -82px; }
|
|
| 1217 | 1228 |
} |
| 1218 | 1229 |
@media (max-width: 430px) {
|
| 1219 | 1230 |
#login-screen { padding: 24px 16px 120px; }
|
@@ -1223,12 +1234,13 @@ sub app_html {
|
||
| 1223 | 1234 |
padding: 36px 22px 34px; |
| 1224 | 1235 |
} |
| 1225 | 1236 |
.otp-row input { height: 52px; }
|
| 1226 |
- .otp-row { margin-bottom: 88px; }
|
|
| 1237 |
+ .login-card form { padding-bottom: 88px; }
|
|
| 1227 | 1238 |
} |
| 1228 | 1239 |
@media (max-height: 720px) {
|
| 1229 | 1240 |
#login-screen { padding-top: 28px; padding-bottom: 96px; }
|
| 1230 | 1241 |
.login-card { padding-top: 34px; padding-bottom: 34px; gap: 20px; }
|
| 1231 |
- .otp-row { margin-bottom: 72px; }
|
|
| 1242 |
+ .login-card form { padding-bottom: 72px; }
|
|
| 1243 |
+ #login-error { margin-top: -58px; }
|
|
| 1232 | 1244 |
} |
| 1233 | 1245 |
|
| 1234 | 1246 |
/* ── App shell (hidden until authenticated) ── */ |
@@ -1280,10 +1292,13 @@ sub app_html {
|
||
| 1280 | 1292 |
<div class="login-card"> |
| 1281 | 1293 |
<div class="brand"> |
| 1282 | 1294 |
<div class="icon"> |
| 1283 |
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> |
|
| 1284 |
- <path d="M20 3H4a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm-1 5H5V5h14v3zm1 5H4a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1zm-1 5H5v-3h14v3z"/> |
|
| 1285 |
- <circle cx="17" cy="7" r="1"/><circle cx="19.5" cy="7" r="1"/> |
|
| 1286 |
- <circle cx="17" cy="15.5" r="1"/><circle cx="19.5" cy="15.5" r="1"/> |
|
| 1295 |
+ <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> |
|
| 1296 |
+ <rect x="16" y="10" width="32" height="44" rx="4"/> |
|
| 1297 |
+ <rect x="21" y="16" width="22" height="8" rx="2"/> |
|
| 1298 |
+ <rect x="21" y="28" width="22" height="8" rx="2"/> |
|
| 1299 |
+ <rect x="21" y="40" width="22" height="8" rx="2"/> |
|
| 1300 |
+ <path d="M26 20h8M26 32h8M26 44h8"/> |
|
| 1301 |
+ <path d="M40 20h.01M40 32h.01M40 44h.01"/> |
|
| 1287 | 1302 |
</svg> |
| 1288 | 1303 |
</div> |
| 1289 | 1304 |
<h1>Host Manager</h1> |
@@ -1299,7 +1314,6 @@ sub app_html {
|
||
| 1299 | 1314 |
<input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" class="otp-digit"> |
| 1300 | 1315 |
<input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]" class="otp-digit"> |
| 1301 | 1316 |
</div> |
| 1302 |
- <button class="primary" type="submit" id="login-btn">Autentifică-te</button> |
|
| 1303 | 1317 |
</form> |
| 1304 | 1318 |
<div id="login-error"></div> |
| 1305 | 1319 |
</div> |
@@ -1603,7 +1617,7 @@ sub app_html {
|
||
| 1603 | 1617 |
input.value = val; |
| 1604 | 1618 |
input.classList.toggle('filled', !!val);
|
| 1605 | 1619 |
if (val && idx < otpDigits.length - 1) otpDigits[idx + 1].focus(); |
| 1606 |
- if (val && idx === otpDigits.length - 1) $('login-form').requestSubmit();
|
|
| 1620 |
+ if (val && idx === otpDigits.length - 1 && otpReady()) $('login-form').requestSubmit();
|
|
| 1607 | 1621 |
}); |
| 1608 | 1622 |
input.addEventListener('paste', (e) => {
|
| 1609 | 1623 |
const text = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
|
@@ -1614,17 +1628,18 @@ sub app_html {
|
||
| 1614 | 1628 |
}); |
| 1615 | 1629 |
const next = Math.min(text.length, otpDigits.length - 1); |
| 1616 | 1630 |
otpDigits[next].focus(); |
| 1617 |
- if (text.length >= otpDigits.length) $('login-form').requestSubmit();
|
|
| 1631 |
+ if (otpReady()) $('login-form').requestSubmit();
|
|
| 1618 | 1632 |
}); |
| 1619 | 1633 |
}); |
| 1620 | 1634 |
|
| 1621 | 1635 |
function getOtp() { return otpDigits.map(i => i.value).join(''); }
|
| 1636 |
+ function otpReady() { return otpDigits.every(i => /^\d$/.test(i.value)); }
|
|
| 1622 | 1637 |
function clearOtp() { otpDigits.forEach(i => { i.value = ''; i.classList.remove('filled'); }); otpDigits[0].focus(); }
|
| 1623 | 1638 |
|
| 1624 | 1639 |
$('login-form').addEventListener('submit', async (event) => {
|
| 1625 | 1640 |
event.preventDefault(); |
| 1626 |
- const btn = $('login-btn');
|
|
| 1627 |
- btn.disabled = true; |
|
| 1641 |
+ if (!otpReady()) return; |
|
| 1642 |
+ $('login-form').classList.add('busy');
|
|
| 1628 | 1643 |
$('login-error').textContent = '';
|
| 1629 | 1644 |
try {
|
| 1630 | 1645 |
await api('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ otp: getOtp() }) });
|
@@ -1632,7 +1647,7 @@ sub app_html {
|
||
| 1632 | 1647 |
} catch (e) {
|
| 1633 | 1648 |
showLogin(e.message === 'invalid_otp' ? 'Cod incorect.' : e.message); |
| 1634 | 1649 |
} finally {
|
| 1635 |
- btn.disabled = false; |
|
| 1650 |
+ $('login-form').classList.remove('busy');
|
|
| 1636 | 1651 |
} |
| 1637 | 1652 |
}); |
| 1638 | 1653 |
|