PIN pads: an Unlock button, Next to set 6-8 digits, bigger keys
A PIN can be 6, 7 or 8 digits, but the pads submitted on the last expected digit, and setting one meant first picking its length. Now: - The unlock prompt fills the dots for the PIN's length and sends it with Unlock (or Enter) beside Cancel, so a slip can still be fixed with the delete key first. - Setting a PIN takes 6 to 8 digits with Next (the last two dots are marked optional), and the repeat step saves with Save PIN once it has as many. - Keys are larger (76x56, 24px digits) and the dots a little bigger.
This commit is contained in:
parent
ed441b4e9d
commit
c4b3e77238
2 changed files with 44 additions and 24 deletions
|
|
@ -272,15 +272,17 @@
|
|||
.pinwiz-box input[type=password]{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:8px;border:1px solid var(--line);background:#1b2330;color:var(--ink);font:inherit;font-size:14px}
|
||||
.pinwiz-err{color:#f6768a;font-size:12.5px;line-height:18px;min-height:36px;margin:8px 0 2px;text-align:center}
|
||||
.pinwiz-act{display:flex;justify-content:space-between;gap:8px;margin-top:6px}
|
||||
.pinwiz-act .btn:disabled{opacity:.45;cursor:default}
|
||||
.pinwiz-len{display:flex;justify-content:center;gap:6px;margin:0 0 12px}
|
||||
.pinwiz-len button{border:1px solid var(--line);background:transparent;color:var(--mut);border-radius:999px;padding:4px 12px;font-size:12.5px;cursor:pointer}
|
||||
.pinwiz-len button.on{border-color:var(--acid);color:var(--acid-text);background:rgb(from var(--acid) r g b / .12)}
|
||||
.pinpad{display:flex;flex-direction:column;align-items:center;gap:14px}
|
||||
.pinpad .pindots{display:flex;gap:12px}
|
||||
.pinpad .pindot{width:12px;height:12px;border-radius:50%;border:1.5px solid var(--dim);transition:background .12s,border-color .12s}
|
||||
.pinpad .pindot{width:14px;height:14px;border-radius:50%;border:1.5px solid var(--dim);transition:background .12s,border-color .12s}
|
||||
.pinpad .pindot.optional{border-style:dashed;opacity:.6} /* digits 7 and 8: allowed, not needed */
|
||||
.pinpad .pindot.on{background:var(--acid);border-color:var(--acid)}
|
||||
.pinpad .pinkeys{display:grid;grid-template-columns:repeat(3,62px);gap:8px}
|
||||
.pinpad .pinkeys button{height:46px;border-radius:10px;border:1px solid var(--line);background:#1b2330;color:var(--ink);font:500 18px system-ui,sans-serif;cursor:pointer}
|
||||
.pinpad .pinkeys{display:grid;grid-template-columns:repeat(3,76px);gap:10px}
|
||||
.pinpad .pinkeys button{height:56px;border-radius:12px;border:1px solid var(--line);background:#1b2330;color:var(--ink);font:500 24px system-ui,sans-serif;cursor:pointer}
|
||||
/* Keys only lighten on hover; an acid border lighting up on each key the pointer crossed read as flashing. */
|
||||
.pinpad .pinkeys button{transition:background-color .08s,transform .06s}
|
||||
.pinpad .pinkeys button:hover{background:#232c3b}
|
||||
|
|
@ -2354,9 +2356,9 @@
|
|||
// the dialog closes.
|
||||
const pinWiz = {
|
||||
el: document.getElementById("pinWiz"),
|
||||
step: 0, master: "", first: "", entry: "", len: 6, busy: false,
|
||||
step: 0, master: "", first: "", entry: "", busy: false,
|
||||
open() {
|
||||
Object.assign(this, { step: 1, master: "", first: "", entry: "", len: 6, busy: false });
|
||||
Object.assign(this, { step: 1, master: "", first: "", entry: "", busy: false });
|
||||
this.el.hidden = false;
|
||||
this.render();
|
||||
},
|
||||
|
|
@ -2365,6 +2367,11 @@
|
|||
this.el.hidden = true;
|
||||
},
|
||||
error(msg) { document.getElementById("pinWizErr").textContent = msg || ""; },
|
||||
// Step 2 takes 6 to 8 digits (eight dots, the first six marked as
|
||||
// needed); step 3 must repeat exactly as many. Next, or Enter, moves on;
|
||||
// nothing advances by itself, so a slip can still be fixed with ⌫.
|
||||
get len() { return this.step === 3 ? this.first.length : 8; },
|
||||
ready() { return this.step === 3 ? this.entry.length === this.first.length : this.entry.length >= 6; },
|
||||
press(k) {
|
||||
if (this.busy || (this.step !== 2 && this.step !== 3)) return;
|
||||
if (k === "back") this.entry = this.entry.slice(0, -1);
|
||||
|
|
@ -2372,14 +2379,15 @@
|
|||
else if (this.entry.length < this.len) this.entry += k;
|
||||
this.error("");
|
||||
this.paintDots();
|
||||
if (this.entry.length === this.len) setTimeout(() => this.next(), 120);
|
||||
},
|
||||
paintDots() {
|
||||
document.querySelectorAll("#pinWizBody .pindot").forEach((d, i) => d.classList.toggle("on", i < this.entry.length));
|
||||
const nextBtn = document.getElementById("pinWizNext");
|
||||
if (this.step === 2 || this.step === 3) nextBtn.disabled = !this.ready();
|
||||
},
|
||||
pad() {
|
||||
const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "clear", "0", "back"];
|
||||
return `<div class="pinpad"><div class="pindots">${Array.from({ length: this.len }, () => `<span class="pindot"></span>`).join("")}</div>` +
|
||||
return `<div class="pinpad"><div class="pindots">${Array.from({ length: this.len }, (_, i) => `<span class="pindot${this.step === 2 && i >= 6 ? " optional" : ""}"></span>`).join("")}</div>` +
|
||||
`<div class="pinkeys">${keys.map((k) => `<button type="button" data-k="${k}" class="${k === "back" || k === "clear" ? "util" : ""}" aria-label="${k === "back" ? "Delete" : k === "clear" ? "Clear" : k}">${k === "back" ? "⌫" : k === "clear" ? "Clear" : k}</button>`).join("")}</div></div>`;
|
||||
},
|
||||
render() {
|
||||
|
|
@ -2397,16 +2405,14 @@
|
|||
nextBtn.hidden = false; nextBtn.textContent = "Next";
|
||||
} else if (this.step === 2) {
|
||||
title.textContent = "Choose a PIN";
|
||||
text.textContent = "Pick how many digits, then type them. Longer is stronger.";
|
||||
body.innerHTML = `<div class="pinwiz-len">${[6, 7, 8].map((n) => `<button type="button" data-len="${n}" class="${n === this.len ? "on" : ""}">${n} digits</button>`).join("")}</div>` + this.pad();
|
||||
body.querySelectorAll("[data-len]").forEach((b) => b.onclick = () => { this.len = +b.dataset.len; this.entry = ""; this.render(); });
|
||||
nextBtn.hidden = true;
|
||||
text.textContent = "Type 6 to 8 digits, then press Next. Longer is stronger.";
|
||||
body.innerHTML = this.pad();
|
||||
nextBtn.hidden = false; nextBtn.textContent = "Next";
|
||||
} else if (this.step === 3) {
|
||||
title.textContent = "Repeat your PIN";
|
||||
text.textContent = `Type the same ${this.len} digits again.`;
|
||||
// The same (hidden) length row as step 2, so the pad does not move up.
|
||||
body.innerHTML = `<div class="pinwiz-len" style="visibility:hidden" aria-hidden="true"><button type="button" tabindex="-1">8 digits</button></div>` + this.pad();
|
||||
nextBtn.hidden = true;
|
||||
text.textContent = `Type the same ${this.len} digits again, then press Save.`;
|
||||
body.innerHTML = this.pad();
|
||||
nextBtn.hidden = false; nextBtn.textContent = "Save PIN";
|
||||
} else if (this.step === 4) {
|
||||
title.textContent = "PIN set";
|
||||
text.textContent = this.hardware === "tpm"
|
||||
|
|
@ -2416,6 +2422,8 @@
|
|||
nextBtn.hidden = false; nextBtn.textContent = "Done";
|
||||
}
|
||||
body.querySelectorAll("[data-k]").forEach((b) => b.onclick = () => this.press(b.dataset.k));
|
||||
nextBtn.disabled = false;
|
||||
if (this.step === 2 || this.step === 3) this.paintDots(); // Next waits for enough digits
|
||||
document.getElementById("pinWizCancel").hidden = this.step === 4;
|
||||
},
|
||||
async next() {
|
||||
|
|
@ -2429,10 +2437,10 @@
|
|||
if (!r.ok) return this.error("Wrong master password.");
|
||||
this.master = pw; this.step = 2; this.render();
|
||||
} else if (this.step === 2) {
|
||||
if (this.entry.length !== this.len) return;
|
||||
if (!this.ready()) return this.error("Type at least 6 digits.");
|
||||
this.first = this.entry; this.entry = ""; this.step = 3; this.render();
|
||||
} else if (this.step === 3) {
|
||||
if (this.entry.length !== this.len) return;
|
||||
if (!this.ready()) return;
|
||||
if (this.entry !== this.first) {
|
||||
this.first = ""; this.entry = ""; this.step = 2; this.render();
|
||||
return this.error("The PINs didn't match. Choose your PIN again.");
|
||||
|
|
@ -2459,6 +2467,7 @@
|
|||
if (e.key === "Escape") return pinWiz.close();
|
||||
if (/^[0-9]$/.test(e.key)) pinWiz.press(e.key);
|
||||
else if (e.key === "Backspace" && (pinWiz.step === 2 || pinWiz.step === 3)) pinWiz.press("back");
|
||||
else if (e.key === "Enter" && (pinWiz.step === 2 || pinWiz.step === 3)) { e.preventDefault(); pinWiz.next(); }
|
||||
});
|
||||
document.getElementById("pinClearBtn").onclick = async () => {
|
||||
if (!confirm("Remove the PIN? The vault will need the master password again.")) return;
|
||||
|
|
|
|||
25
unlock.html
25
unlock.html
|
|
@ -27,14 +27,14 @@
|
|||
/* The same PIN pad as Aegis (panel.html .pinpad): one PIN, one screen. */
|
||||
.pinpad { display: flex; flex-direction: column; align-items: center; gap: 14px; margin: 6px 0 12px; }
|
||||
.pinpad .pindots { display: flex; gap: 12px; }
|
||||
.pinpad .pindot { width: 12px; height: 12px; border-radius: 50%; border: 1.5px solid var(--dim);
|
||||
.pinpad .pindot { width: 14px; height: 14px; border-radius: 50%; border: 1.5px solid var(--dim);
|
||||
background: transparent; transition: background .12s, border-color .12s; }
|
||||
/* No glow, and keys only lighten on hover: a bright acid border lighting
|
||||
up on whichever key the pointer crosses read as the pad flashing. */
|
||||
.pinpad .pindot.on { background: var(--acid); border-color: var(--acid); }
|
||||
.pinpad .pinkeys { display: grid; grid-template-columns: repeat(3, 62px); gap: 8px; }
|
||||
.pinpad .pinkeys button { height: 46px; border-radius: 10px; border: 1px solid var(--line);
|
||||
background: var(--surface2); color: var(--ink); font: 500 18px system-ui, sans-serif; cursor: pointer; }
|
||||
.pinpad .pinkeys { display: grid; grid-template-columns: repeat(3, 76px); gap: 10px; }
|
||||
.pinpad .pinkeys button { height: 56px; border-radius: 12px; border: 1px solid var(--line);
|
||||
background: var(--surface2); color: var(--ink); font: 500 24px system-ui, sans-serif; cursor: pointer; }
|
||||
.pinpad .pinkeys button { transition: background-color .08s, transform .06s; }
|
||||
.pinpad .pinkeys button:hover { background: rgb(from var(--surface2) calc(r + 10) calc(g + 10) calc(b + 12)); }
|
||||
.pinpad .pinkeys button:active { transform: scale(.96); }
|
||||
|
|
@ -49,6 +49,8 @@
|
|||
input:focus, button:focus-visible { outline: 2px solid rgb(from var(--acid) r g b / .6); outline-offset: 1px; }
|
||||
.err { color: var(--danger); font-size: 12.5px; line-height: 18px; min-height: 36px; margin-bottom: 6px; text-align: center; }
|
||||
.pact { display: flex; gap: 6px; justify-content: space-between; align-items: center; }
|
||||
.pbtns { display: flex; gap: 6px; }
|
||||
.pbtn.primary:disabled { background: var(--surface2); color: var(--dim); border-color: var(--line); opacity: 1; }
|
||||
.link { background: none; border: none; color: var(--mut); cursor: pointer; font: inherit; font-size: 12.5px; padding: 0; text-decoration: underline; }
|
||||
.pbtn { padding: 7px 14px; border-radius: 7px; border: 1px solid var(--line); background: var(--surface2);
|
||||
color: var(--ink); cursor: pointer; font: inherit; font-size: 12.5px; }
|
||||
|
|
@ -108,7 +110,6 @@
|
|||
else if (pin.length < pinLength) pin += d;
|
||||
error = "";
|
||||
paint();
|
||||
if (pin.length === pinLength) submit(pin);
|
||||
}
|
||||
|
||||
// Keypresses and answers update the dialog in place. Rebuilding it on
|
||||
|
|
@ -121,7 +122,10 @@
|
|||
if (ui.dots) ui.dots.forEach((d, i) => d.classList.toggle("on", i < pin.length));
|
||||
ui.err.textContent = error;
|
||||
if (ui.pad) ui.pad.classList.toggle("busy", busy);
|
||||
if (ui.go) { ui.go.textContent = busy ? "Checking…" : "Unlock"; ui.go.disabled = busy; }
|
||||
if (ui.go) {
|
||||
ui.go.textContent = busy ? "Checking…" : "Unlock";
|
||||
ui.go.disabled = busy || (mode === "pin" && pin.length !== pinLength);
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
|
|
@ -144,10 +148,16 @@
|
|||
ui.dots = [...dots.children];
|
||||
ui.pad = pad;
|
||||
ui.err = el("div", { class: "pinerr", role: "alert" }, error);
|
||||
// The PIN is sent with Unlock (or Enter) once every digit is in, not on
|
||||
// the last keypress, so a slip can still be corrected with ⌫.
|
||||
ui.go = el("button", { class: "pbtn primary", type: "button", onclick: () => { if (pin.length === pinLength) submit(pin); } }, "Unlock");
|
||||
ui.go.disabled = pin.length !== pinLength;
|
||||
bodyEls = [el("div", { class: "pinpad" }, dots, pad, ui.err),
|
||||
el("div", { class: "pact" },
|
||||
el("button", { class: "link", type: "button", onclick: () => { mode = "password"; error = ""; pin = ""; render(); } }, "Use master password"),
|
||||
el("button", { class: "pbtn", type: "button", onclick: () => done(true) }, "Cancel"))];
|
||||
el("div", { class: "pbtns" },
|
||||
el("button", { class: "pbtn", type: "button", onclick: () => done(true) }, "Cancel"),
|
||||
ui.go))];
|
||||
} else {
|
||||
const input = el("input", { type: "password", placeholder: "Master password", autocomplete: "current-password" });
|
||||
const go = el("button", { class: "pbtn primary", type: "submit" }, busy ? "Checking…" : "Unlock");
|
||||
|
|
@ -182,6 +192,7 @@
|
|||
if (e.key === "Escape") return done(true);
|
||||
if (mode === "pin" && /^[0-9]$/.test(e.key)) press(e.key);
|
||||
else if (mode === "pin" && e.key === "Backspace") press("back");
|
||||
else if (mode === "pin" && e.key === "Enter") { e.preventDefault(); if (pin.length === pinLength && !busy) submit(pin); }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue