helwasm: hierarchical quick-gen, pure sugar over the engine
Quick form now drives the one shared engine through commands only, so it and the console give identical results (form == console). - password hook answers only the "/" prompt, so read_master climbs and computes every ^parent from a single root master (Req 4) - new master in the field is authoritative via `unpass`; empty field reuses the stored session master, so later names generate without re-typing it (Req 1/2) - `unpass` with no name now clears ALL cached masters (real command, not a private hook) so the form stays reproducible in the console - result colour = root master marked correct (correct / / uncorrect /), derived live from the engine; black otherwise (Req 3) - Mark-correct toggle button; click the master to reveal it; honest memory-only hint - master stays in memory only, never written to storage; only the one-way correct hash is saved on the device parser: bare-name forms now keep a trailing comment, and a ^-led token is never a name, so `name ^parent` / `name MODE ^parent` link the parent instead of qpname reading it as prefix+name (was silently mis-stored).
This commit is contained in:
+34
-10
@@ -38,17 +38,20 @@ peg::parser! {
|
|||||||
rule mname() -> Password = &(word() _ word() _ num()? mode() _ date()) pr:word() _ pn:word() _ pl:num()? pm:mode() _ pd:date() pc:comment()?
|
rule mname() -> Password = &(word() _ word() _ num()? mode() _ date()) pr:word() _ pn:word() _ pl:num()? pm:mode() _ pd:date() pc:comment()?
|
||||||
{ Password::new(Some(pr), pn, pl, pm, 99, pd, pc) }
|
{ Password::new(Some(pr), pn, pl, pm, 99, pd, pc) }
|
||||||
// prefix + name + [len]mode (no seq/date) -> defaults seq 99, date now
|
// prefix + name + [len]mode (no seq/date) -> defaults seq 99, date now
|
||||||
rule npname() -> Password = &(word() _ word() _ num()? mode()) pr:word() _ pn:word() _ pl:num()? pm:mode()
|
rule npname() -> Password = &(word() _ word() _ num()? mode()) pr:word() _ pn:word() _ pl:num()? pm:mode() pc:comment()?
|
||||||
{ Password::new(Some(pr), pn, pl, pm, 99, Date::now(), None) }
|
{ Password::new(Some(pr), pn, pl, pm, 99, Date::now(), pc) }
|
||||||
rule sname() -> Password = &(word() _ num()? mode() _ date()) pn:word() _ pl:num()? pm:mode() _ pd:date() pc:comment()?
|
rule sname() -> Password = &(word() _ num()? mode() _ date()) pn:word() _ pl:num()? pm:mode() _ pd:date() pc:comment()?
|
||||||
{ Password::new(None, pn, pl, pm, 99, pd, pc) }
|
{ Password::new(None, pn, pl, pm, 99, pd, pc) }
|
||||||
rule nname() -> Password = &(word() _ num()? mode()) pn:word() _ pl:num()? pm:mode()
|
rule nname() -> Password = &(word() _ num()? mode()) pn:word() _ pl:num()? pm:mode() pc:comment()?
|
||||||
{ Password::new(None, pn, pl, pm, 99, Date::now(), None) }
|
{ Password::new(None, pn, pl, pm, 99, Date::now(), pc) }
|
||||||
// prefix + name only -> defaults mode R, seq 99, date now
|
// prefix + name (+ optional comment) -> defaults mode R, seq 99, date now.
|
||||||
rule qpname() -> Password = &(word() _ word()) pr:word() _ pn:word()
|
// A `^parent` token is never a name, so a `^`-leading second word fails here and
|
||||||
{ Password::new(Some(pr), pn, None, Mode::Regular, 99, Date::now(), None) }
|
// falls through to qname, which keeps it as the comment (the parent marker).
|
||||||
rule qname() -> Password = &(word()) pn:word()
|
rule qpname() -> Password = &(word() _ word()) pr:word() _ pn:$(!"^" ['!'..='~']+) pc:comment()?
|
||||||
{ Password::new(None, pn, None, Mode::Regular, 99, Date::now(), None) }
|
{ Password::new(Some(pr), pn.to_string(), None, Mode::Regular, 99, Date::now(), pc) }
|
||||||
|
// name (+ optional comment) -> e.g. `github` or `github ^work`
|
||||||
|
rule qname() -> Password = pn:word() pc:comment()?
|
||||||
|
{ Password::new(None, pn, None, Mode::Regular, 99, Date::now(), pc) }
|
||||||
pub rule name() -> Password = name:(jname() / pname() / mname() / npname() / sname() / nname() / qpname() / qname())? {?
|
pub rule name() -> Password = name:(jname() / pname() / mname() / npname() / sname() / nname() / qpname() / qname())? {?
|
||||||
match name { Some(n) => Ok(n), None => Err("failed to parse password description") }
|
match name { Some(n) => Ok(n), None => Err("failed to parse password description") }
|
||||||
}
|
}
|
||||||
@@ -107,7 +110,7 @@ peg::parser! {
|
|||||||
rule pass_cmd() -> Command<'input> = p:(pass_long_cmd() / pass_short_cmd()) { p }
|
rule pass_cmd() -> Command<'input> = p:(pass_long_cmd() / pass_short_cmd()) { p }
|
||||||
rule correct_cmd() -> Command<'input> = "correct" _ name:word() { Command::Correct(name) }
|
rule correct_cmd() -> Command<'input> = "correct" _ name:word() { Command::Correct(name) }
|
||||||
rule uncorrect_cmd() -> Command<'input> = "uncorrect" _ name:word() { Command::Uncorrect(name) }
|
rule uncorrect_cmd() -> Command<'input> = "uncorrect" _ name:word() { Command::Uncorrect(name) }
|
||||||
rule unpass_cmd() -> Command<'input> = "unpass" _ name:word() { Command::UnPass(name) }
|
rule unpass_cmd() -> Command<'input> = "unpass" name:(_ w:word() { w })? { Command::UnPass(name) }
|
||||||
rule enc_cmd() -> Command<'input> = "enc" _ name:word() { Command::Enc(name) }
|
rule enc_cmd() -> Command<'input> = "enc" _ name:word() { Command::Enc(name) }
|
||||||
rule rm_cmd() -> Command<'input> = "rm" _ name:word() { Command::Rm(name) }
|
rule rm_cmd() -> Command<'input> = "rm" _ name:word() { Command::Rm(name) }
|
||||||
rule comment_cmd() -> Command<'input> = "comment" _ name:word() c:comment()? { Command::Comment(name, c) }
|
rule comment_cmd() -> Command<'input> = "comment" _ name:word() c:comment()? { Command::Comment(name, c) }
|
||||||
@@ -245,6 +248,27 @@ add t3 C 99 2022-12-14
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_short_parent_test() {
|
||||||
|
// `name ^parent` (no mode/date): name is the name, ^parent stays in the comment
|
||||||
|
// for fix_hierarchy — it must NOT be read as prefix+name.
|
||||||
|
let p = command_parser::name("x ^acc").unwrap();
|
||||||
|
assert_eq!(p.name, "x");
|
||||||
|
assert_eq!(p.prefix, None);
|
||||||
|
assert_eq!(p.comment, Some("^acc".to_string()));
|
||||||
|
// `name [len]mode ^parent` keeps the parent in the comment too
|
||||||
|
let p2 = command_parser::name("x 20R ^acc").unwrap();
|
||||||
|
assert_eq!(p2.name, "x");
|
||||||
|
assert_eq!(p2.length, Some(20));
|
||||||
|
assert_eq!(p2.mode, Mode::Regular);
|
||||||
|
assert_eq!(p2.comment, Some("^acc".to_string()));
|
||||||
|
// a real prefix + name (second word not ^-led) still parses as prefix+name
|
||||||
|
let p3 = command_parser::name("#W9 github").unwrap();
|
||||||
|
assert_eq!(p3.prefix, Some("#W9".to_string()));
|
||||||
|
assert_eq!(p3.name, "github");
|
||||||
|
assert_eq!(p3.comment, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_password_test() {
|
fn parse_password_test() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+6
-2
@@ -133,10 +133,14 @@ impl<'a> LKEval<'a> {
|
|||||||
Command::Set(key, value) => { to_history = false; self.cmd_set(&out, key, value); }
|
Command::Set(key, value) => { to_history = false; self.cmd_set(&out, key, value); }
|
||||||
Command::Pass(name, None) => self.cmd_pass(&out, &name, &None),
|
Command::Pass(name, None) => self.cmd_pass(&out, &name, &None),
|
||||||
Command::Pass(name, pass) => { to_history = false; self.cmd_pass(&out, &name, &pass); },
|
Command::Pass(name, pass) => { to_history = false; self.cmd_pass(&out, &name, &pass); },
|
||||||
Command::UnPass(name) => match self.state.lock().borrow_mut().secrets.remove(name) {
|
Command::UnPass(Some(name)) => match self.state.lock().borrow_mut().secrets.remove(name) {
|
||||||
Some(_) => out.o(format!("Removed saved password for {}", name)),
|
Some(_) => out.o(format!("Removed saved password for {}", name)),
|
||||||
None => out.e(format!("error: saved password for {} not found", name)),
|
None => out.e(format!("error: saved password for {} not found", name)),
|
||||||
},
|
},
|
||||||
|
Command::UnPass(None) => {
|
||||||
|
self.state.lock().borrow_mut().secrets.clear();
|
||||||
|
out.o("forgot all cached masters".to_string());
|
||||||
|
},
|
||||||
Command::Correct(name) => self.cmd_correct(&out, name, true, None),
|
Command::Correct(name) => self.cmd_correct(&out, name, true, None),
|
||||||
Command::Uncorrect(name) => self.cmd_correct(&out, name, false, None),
|
Command::Uncorrect(name) => self.cmd_correct(&out, name, false, None),
|
||||||
Command::Noop => { to_history = false; },
|
Command::Noop => { to_history = false; },
|
||||||
@@ -156,7 +160,7 @@ impl<'a> LKEval<'a> {
|
|||||||
" enc <name> show the generated password\n",
|
" enc <name> show the generated password\n",
|
||||||
" gen[N] <name> N numbered variants; name ends in G.. (all) or X.. (random)\n",
|
" gen[N] <name> N numbered variants; name ends in G.. (all) or X.. (random)\n",
|
||||||
" pass <name> [pw] cache a master / override for an entry's subtree\n",
|
" pass <name> [pw] cache a master / override for an entry's subtree\n",
|
||||||
" unpass <name> forget a cached password (unpass / = the root master)\n",
|
" unpass [name] forget a cached password (unpass / = root; unpass = all)\n",
|
||||||
" correct <name> trust this password's hash uncorrect <name> untrust it\n",
|
" correct <name> trust this password's hash uncorrect <name> untrust it\n",
|
||||||
"\n",
|
"\n",
|
||||||
"catalog\n",
|
"catalog\n",
|
||||||
|
|||||||
+3
-2
@@ -97,7 +97,7 @@ pub enum Command<'a> {
|
|||||||
Enc(Name),
|
Enc(Name),
|
||||||
Gen(u32, PasswordRef),
|
Gen(u32, PasswordRef),
|
||||||
Pass(Name, Option<String>),
|
Pass(Name, Option<String>),
|
||||||
UnPass(Name),
|
UnPass(Option<Name>),
|
||||||
Correct(Name),
|
Correct(Name),
|
||||||
Uncorrect(Name),
|
Uncorrect(Name),
|
||||||
PasteBuffer(String),
|
PasteBuffer(String),
|
||||||
@@ -153,7 +153,8 @@ impl<'a> std::fmt::Display for Command<'a> {
|
|||||||
Command::Gen(a, b) => write!(f, "gen{} {}", a, b.lock().borrow().to_string().trim()),
|
Command::Gen(a, b) => write!(f, "gen{} {}", a, b.lock().borrow().to_string().trim()),
|
||||||
Command::Pass(a, None) => write!(f, "pass {}", a),
|
Command::Pass(a, None) => write!(f, "pass {}", a),
|
||||||
Command::Pass(a, Some(b)) => write!(f, "pass {} {}", a, b),
|
Command::Pass(a, Some(b)) => write!(f, "pass {} {}", a, b),
|
||||||
Command::UnPass(s) => write!(f, "unpass {}", s),
|
Command::UnPass(None) => write!(f, "unpass"),
|
||||||
|
Command::UnPass(Some(s)) => write!(f, "unpass {}", s),
|
||||||
Command::Correct(s) => write!(f, "correct {}", s),
|
Command::Correct(s) => write!(f, "correct {}", s),
|
||||||
Command::Uncorrect(s) => write!(f, "uncorrect {}", s),
|
Command::Uncorrect(s) => write!(f, "uncorrect {}", s),
|
||||||
Command::PasteBuffer(s) => write!(f, "pb {}", s),
|
Command::PasteBuffer(s) => write!(f, "pb {}", s),
|
||||||
|
|||||||
+72
-25
@@ -66,8 +66,10 @@
|
|||||||
<h4>Quick password</h4>
|
<h4>Quick password</h4>
|
||||||
<p>
|
<p>
|
||||||
Type an account name and your master phrase. The password appears instantly,
|
Type an account name and your master phrase. The password appears instantly,
|
||||||
masked. Click it to reveal, or press <strong>Copy</strong>. <strong>Store</strong>
|
masked; click it (or the master) to reveal. Press <strong>Copy</strong>, or
|
||||||
remembers the <em>name</em> (never the password) in this browser.
|
<strong>Store</strong> to remember the <em>name</em> (never the password).
|
||||||
|
Click <strong>Mark correct</strong> once and the right master shows in colour,
|
||||||
|
so a typo stays black.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -85,6 +87,8 @@
|
|||||||
After a name you can add a length and a mode. <code>R</code> is six memorable words
|
After a name you can add a length and a mode. <code>R</code> is six memorable words
|
||||||
(the default); <code>C</code> camel; <code>H</code> hex; <code>B</code> base64;
|
(the default); <code>C</code> camel; <code>H</code> hex; <code>B</code> base64;
|
||||||
<code>D</code> digits (<code>U…</code> = upper). For example, <code>github 20R</code>.
|
<code>D</code> digits (<code>U…</code> = upper). For example, <code>github 20R</code>.
|
||||||
|
End a name with <code>^folder</code> to derive it from a parent; you still type only
|
||||||
|
your one master and the whole chain is computed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,10 +104,11 @@
|
|||||||
<div class="eyebrow">Generate</div>
|
<div class="eyebrow">Generate</div>
|
||||||
<h2>Quick password</h2>
|
<h2>Quick password</h2>
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
The same name and master always make the same password, generated live in
|
The same name and master always make the same password, computed live in your
|
||||||
your browser. Nothing is saved unless you press <strong>Store</strong>, and
|
browser. Your master is kept in memory for this tab only, never written to
|
||||||
then only in this browser on your device, never on a server. The app never
|
storage, and the app never talks to a server. <strong>Store</strong> saves
|
||||||
communicates with a server at all.
|
just the entry name; <strong>Mark correct</strong> saves only a one-way hash,
|
||||||
|
both on this device.
|
||||||
</p>
|
</p>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -112,7 +117,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="master">Master password</label>
|
<label for="master">Master password</label>
|
||||||
<input type="text" id="master" class="mask" placeholder="your master phrase" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" data-1p-ignore data-lpignore="true" data-bwignore="true" />
|
<input type="text" id="master" class="mask" placeholder="your master phrase" title="click to show or hide" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" data-1p-ignore data-lpignore="true" data-bwignore="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result">
|
<div class="result">
|
||||||
@@ -120,6 +125,7 @@
|
|||||||
<span class="secret" id="qsecret" title="click to reveal"></span>
|
<span class="secret" id="qsecret" title="click to reveal"></span>
|
||||||
<span class="len" id="qlen"></span>
|
<span class="len" id="qlen"></span>
|
||||||
<span class="actions">
|
<span class="actions">
|
||||||
|
<button class="btn ghost small" id="markCorrect" title="Remember this master as correct on this device, so the right master shows in colour." disabled>Mark correct</button>
|
||||||
<button class="btn small" id="copy">Copy</button>
|
<button class="btn small" id="copy">Copy</button>
|
||||||
<button class="btn ghost small" id="store">Store</button>
|
<button class="btn ghost small" id="store">Store</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -202,7 +208,13 @@
|
|||||||
const CATALOG_KEY = "hel_catalog";
|
const CATALOG_KEY = "hel_catalog";
|
||||||
|
|
||||||
// ---- host imports (wasm calls these by bare name → must be globals) ----
|
// ---- host imports (wasm calls these by bare name → must be globals) ----
|
||||||
window.hel_get_password = () => (masterEl() ? masterEl().value : "") || "";
|
// The master field is the single ROOT master. hel's read_master prompts "/" for
|
||||||
|
// the root and the parent's NAME when climbing a ^parent chain; by answering only
|
||||||
|
// the "/" prompt (and "" otherwise) we force hel to climb to the root and COMPUTE
|
||||||
|
// every intermediate parent from one master — exactly what the CLI does, and the
|
||||||
|
// same for the easy form and the console below, so both give identical results.
|
||||||
|
window.hel_get_password = (prompt) =>
|
||||||
|
prompt === "/" ? (masterEl() ? masterEl().value : "") || "" : "";
|
||||||
window.hel_rnd_range = (s, e) => {
|
window.hel_rnd_range = (s, e) => {
|
||||||
if (e <= s) return s;
|
if (e <= s) return s;
|
||||||
const r = crypto.getRandomValues(new Uint32Array(1))[0] / 4294967296;
|
const r = crypto.getRandomValues(new Uint32Array(1))[0] / 4294967296;
|
||||||
@@ -243,45 +255,77 @@
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- quick generate (live; stateless unless Store) ----
|
// ---- quick generate ----
|
||||||
|
// Pure sugar over the same engine the console drives: it only ever runs
|
||||||
|
// hel_command(...) and DERIVES what it shows from the engine, so you can switch
|
||||||
|
// to the console at any time and keep working on the same catalog, the same
|
||||||
|
// cached master, and the same correct hashes. No private form-only state — the
|
||||||
|
// master lives only in hel's in-memory `secrets` (never written to storage);
|
||||||
|
// only the one-way "correct" hash is persisted on the device.
|
||||||
let lastSecret = "";
|
let lastSecret = "";
|
||||||
|
let appliedMaster = null; // last master value pushed to the engine (debounce hint)
|
||||||
|
function refreshCorrectBtn(enabled, isCorrect) {
|
||||||
|
const b = $("#markCorrect");
|
||||||
|
if (!b) return;
|
||||||
|
b.disabled = !enabled;
|
||||||
|
b.classList.toggle("is-correct", !!enabled && !!isCorrect);
|
||||||
|
b.textContent = enabled && isCorrect ? "Correct ✓" : "Mark correct";
|
||||||
|
b.title = enabled && isCorrect
|
||||||
|
? "This master is remembered as correct on this device. Click to forget it."
|
||||||
|
: "Remember this master as correct on this device, so the right master shows in colour.";
|
||||||
|
}
|
||||||
function quickGen(silent) {
|
function quickGen(silent) {
|
||||||
const spec = $("#qname").value.trim();
|
const spec = $("#qname").value.trim();
|
||||||
const master = masterEl().value;
|
|
||||||
const sec = $("#qsecret");
|
const sec = $("#qsecret");
|
||||||
const lenEl = $("#qlen");
|
const lenEl = $("#qlen");
|
||||||
sec.classList.remove("revealed");
|
sec.classList.remove("revealed");
|
||||||
if (!spec || !master) {
|
|
||||||
sec.textContent = "";
|
|
||||||
lenEl.textContent = "";
|
|
||||||
lastSecret = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The entry name is NOT always the first token; a leading prefix
|
// The entry name is NOT always the first token; a leading prefix
|
||||||
// (like `*P0 test1 …`) means the name is the next word. Parse it.
|
// (like `*P0 test1 …`) means the name is the next word. Parse it.
|
||||||
const name = hel_parse_name(spec);
|
const name = spec ? hel_parse_name(spec) : "";
|
||||||
if (!name) {
|
if (!name) {
|
||||||
sec.textContent = "";
|
sec.textContent = ""; sec.classList.remove("correct");
|
||||||
lenEl.textContent = "";
|
lenEl.textContent = ""; lastSecret = "";
|
||||||
lastSecret = "";
|
refreshCorrectBtn(false, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// hel caches the root master in secrets["/"]; clear it so the live result
|
const master = masterEl().value;
|
||||||
// always reflects the current master field (not a stale cached value).
|
// A new master in the field is authoritative: forget the cached chain so every
|
||||||
hel_command("unpass /");
|
// parent recomputes from it. An empty field keeps whatever master is stored, so
|
||||||
|
// names still generate without re-typing it (the auto-`pass`). `unpass` (no arg)
|
||||||
|
// is the same command you can type in the console.
|
||||||
|
if (master !== appliedMaster) {
|
||||||
|
if (master.length) hel_command("unpass");
|
||||||
|
appliedMaster = master;
|
||||||
|
}
|
||||||
let out = hel_command("enc " + name);
|
let out = hel_command("enc " + name);
|
||||||
if (/not found/.test(out)) {
|
if (/^error: name .* not found/m.test(out)) { // unstored leaf: ephemeral add/enc/rm
|
||||||
hel_command("add " + spec);
|
hel_command("add " + spec);
|
||||||
out = hel_command("enc " + name);
|
out = hel_command("enc " + name);
|
||||||
hel_command("rm " + name);
|
hel_command("rm " + name);
|
||||||
}
|
}
|
||||||
hel_command("unpass /");
|
|
||||||
const pw = stripNoise(out).pop() || "";
|
const pw = stripNoise(out).pop() || "";
|
||||||
|
// Colour = "is the master correct?" — derived from the engine so it reflects
|
||||||
|
// `correct /` / `unpass /` typed in the console too. `enc /` re-emits the root
|
||||||
|
// correctness check; needs secrets["/"], which the gen above just set.
|
||||||
|
const rootChk = hel_command("enc /");
|
||||||
|
const haveRoot = !/^error:/m.test(rootChk);
|
||||||
|
const masterOK = haveRoot && !/warning: password \/ is not marked as correct/.test(rootChk);
|
||||||
lastSecret = pw;
|
lastSecret = pw;
|
||||||
sec.textContent = pw;
|
sec.textContent = pw;
|
||||||
|
sec.classList.toggle("correct", !!pw && masterOK);
|
||||||
lenEl.textContent = pw ? "len " + pw.length : "";
|
lenEl.textContent = pw ? "len " + pw.length : "";
|
||||||
|
refreshCorrectBtn(!!pw && haveRoot, masterOK);
|
||||||
if (!pw && !silent) toast("No output");
|
if (!pw && !silent) toast("No output");
|
||||||
}
|
}
|
||||||
|
function toggleCorrect() {
|
||||||
|
const b = $("#markCorrect");
|
||||||
|
if (!b || b.disabled) return;
|
||||||
|
const wasCorrect = b.classList.contains("is-correct");
|
||||||
|
// Store / remove the root-master hash on the device (same as the console).
|
||||||
|
hel_command(wasCorrect ? "uncorrect /" : "correct /");
|
||||||
|
quickGen(true);
|
||||||
|
toast(wasCorrect ? "Master no longer marked correct" : "Master marked correct");
|
||||||
|
}
|
||||||
function quickStore() {
|
function quickStore() {
|
||||||
const spec = $("#qname").value.trim();
|
const spec = $("#qname").value.trim();
|
||||||
if (!spec) return toast("Enter a name");
|
if (!spec) return toast("Enter a name");
|
||||||
@@ -413,8 +457,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("#qsecret").addEventListener("click", () => $("#qsecret").classList.toggle("revealed"));
|
$("#qsecret").addEventListener("click", () => $("#qsecret").classList.toggle("revealed"));
|
||||||
|
// Click the master field to show what you typed; click again to re-mask.
|
||||||
|
$("#master").addEventListener("click", () => $("#master").classList.toggle("revealed"));
|
||||||
$("#copy").onclick = () => (lastSecret ? copyText(lastSecret) : toast("Nothing to copy"));
|
$("#copy").onclick = () => (lastSecret ? copyText(lastSecret) : toast("Nothing to copy"));
|
||||||
$("#store").onclick = quickStore;
|
$("#store").onclick = quickStore;
|
||||||
|
$("#markCorrect").onclick = toggleCorrect;
|
||||||
|
|
||||||
// pass overlay wiring
|
// pass overlay wiring
|
||||||
$("#passOk").onclick = submitPass;
|
$("#passOk").onclick = submitPass;
|
||||||
|
|||||||
Binary file not shown.
+15
-2
@@ -163,6 +163,10 @@ input:focus { border-color: var(--ocean); box-shadow: 0 0 0 3px rgba(27, 65, 97,
|
|||||||
strong password". Firefox uses the bundled disc font. */
|
strong password". Firefox uses the bundled disc font. */
|
||||||
input.mask { -webkit-text-security: disc; }
|
input.mask { -webkit-text-security: disc; }
|
||||||
@supports not (-webkit-text-security: disc) { input.mask { font-family: "text-security-disc", var(--mono); } }
|
@supports not (-webkit-text-security: disc) { input.mask { font-family: "text-security-disc", var(--mono); } }
|
||||||
|
/* Click the masked master field to reveal what you typed; click again to re-mask. */
|
||||||
|
#master { cursor: pointer; }
|
||||||
|
input.mask.revealed { -webkit-text-security: none; }
|
||||||
|
@supports not (-webkit-text-security: disc) { input.mask.revealed { font-family: var(--mono); } }
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn {
|
.btn {
|
||||||
@@ -177,6 +181,10 @@ input.mask { -webkit-text-security: disc; }
|
|||||||
.btn.ghost { background: transparent; color: var(--ocean); }
|
.btn.ghost { background: transparent; color: var(--ocean); }
|
||||||
.btn.ghost:hover { background: rgba(27, 65, 97, 0.06); color: var(--ocean); border-color: var(--ocean); }
|
.btn.ghost:hover { background: rgba(27, 65, 97, 0.06); color: var(--ocean); border-color: var(--ocean); }
|
||||||
.btn.small { padding: 7px 15px; font-size: 13px; }
|
.btn.small { padding: 7px 15px; font-size: 13px; }
|
||||||
|
/* "Mark correct" toggle: filled ocean once the master is remembered-correct. */
|
||||||
|
.btn.small.is-correct { background: var(--ocean); color: var(--cream); border-color: var(--ocean); }
|
||||||
|
.btn.small.is-correct:hover { background: var(--deep); border-color: var(--deep); }
|
||||||
|
.btn:disabled { opacity: .4; cursor: not-allowed; pointer-events: none; }
|
||||||
|
|
||||||
/* Quick-gen result row */
|
/* Quick-gen result row */
|
||||||
.result {
|
.result {
|
||||||
@@ -185,7 +193,7 @@ input.mask { -webkit-text-security: disc; }
|
|||||||
border-radius: 12px; background: #fff; min-height: 58px;
|
border-radius: 12px; background: #fff; min-height: 58px;
|
||||||
}
|
}
|
||||||
.result .tag { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); }
|
.result .tag { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); }
|
||||||
.result .actions { margin-left: auto; display: flex; gap: 8px; }
|
.result .actions { margin-left: auto; display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
.result .len { font-family: var(--mono); font-size: 11px; color: var(--muted); }
|
.result .len { font-family: var(--mono); font-size: 11px; color: var(--muted); }
|
||||||
/* Quick-gen result: dynamic font (scales down on narrow phones) and wraps so a
|
/* Quick-gen result: dynamic font (scales down on narrow phones) and wraps so a
|
||||||
long password stays inside the card. min-width:0 lets it shrink/wrap within
|
long password stays inside the card. min-width:0 lets it shrink/wrap within
|
||||||
@@ -202,7 +210,12 @@ input.mask { -webkit-text-security: disc; }
|
|||||||
font-size: 19px; color: var(--ink);
|
font-size: 19px; color: var(--ink);
|
||||||
cursor: pointer; user-select: text; white-space: pre;
|
cursor: pointer; user-select: text; white-space: pre;
|
||||||
}
|
}
|
||||||
.secret.revealed { -webkit-text-security: none; color: var(--ocean); }
|
/* Reveal flips masking only; colour is reserved for correctness (below). */
|
||||||
|
.secret.revealed { -webkit-text-security: none; }
|
||||||
|
/* Quick-gen result: ocean = this master is marked correct; otherwise ink ("black"). */
|
||||||
|
.result .secret.correct { color: var(--ocean); font-weight: 500; }
|
||||||
|
/* Console keeps its old cue: a revealed secret turns ocean. */
|
||||||
|
.console-out .secret.revealed { color: var(--ocean); }
|
||||||
/* Firefox lacks -webkit-text-security: fall back to the disc webfont for masking
|
/* Firefox lacks -webkit-text-security: fall back to the disc webfont for masking
|
||||||
(single-line result only; the console table is a WebKit/Blink concern). */
|
(single-line result only; the console table is a WebKit/Blink concern). */
|
||||||
@supports not (-webkit-text-security: disc) {
|
@supports not (-webkit-text-security: disc) {
|
||||||
|
|||||||
Reference in New Issue
Block a user