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); } });