diff --git a/settings.html b/settings.html
index 303517ad..daba19bd 100644
--- a/settings.html
+++ b/settings.html
@@ -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 `
${Array.from({ length: this.len }, () => ``).join("")}
` +
+ return `
${Array.from({ length: this.len }, (_, i) => ``).join("")}
` +
`
${keys.map((k) => ``).join("")}
`;
},
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 = `
${[6, 7, 8].map((n) => ``).join("")}
` + 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 = `
` + 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;
diff --git a/unlock.html b/unlock.html
index efea8c06..ed044ba8 100644
--- a/unlock.html
+++ b/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); }
});