-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpassword-gen.html
More file actions
460 lines (434 loc) · 22.4 KB
/
Copy pathpassword-gen.html
File metadata and controls
460 lines (434 loc) · 22.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Free Password Generator — strong · passphrase · batch — Veriton</title>
<meta name="description" content="Free browser-only password & passphrase generator. Length, charset toggles, exclude ambiguous, crypto randomness, entropy estimate, batch export. No upload, no account."/>
<meta property="og:title" content="Free Password Generator — strong · passphrase · batch — Veriton"/>
<meta property="og:description" content="Generate strong passwords and passphrases in your tab. Crypto-backed randomness, charset controls, entropy estimate, batch copy/download. Nothing leaves the browser."/>
<meta property="og:image" content="https://veriton-dev.github.io/veriton-micro-dev/assets/password-gen-og.jpg"/>
<meta property="og:image:width" content="1280"/>
<meta property="og:image:height" content="720"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="https://veriton-dev.github.io/veriton-micro-dev/assets/password-gen-og.jpg"/>
<link rel="canonical" href="https://veriton-dev.github.io/veriton-micro-dev/tools/password-gen.html"/>
<style>
*{box-sizing:border-box}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;margin:0;color:#14181f;background:#f7f8fa;line-height:1.5}
.wrap{max-width:56rem;margin:0 auto;padding:1.1rem 1rem 2.5rem}
.hero{background:#111827;color:#fff;border-radius:14px;padding:1.3rem 1.2rem;margin-top:.6rem}
.hero h1{margin:0 0 .35rem;font-size:1.45rem}.hero p{margin:.35rem 0;color:#cbd5e1;font-size:.95rem}
.badge{display:inline-block;background:#22c55e;color:#052e16;font-weight:700;font-size:.75rem;padding:.15rem .55rem;border-radius:999px;margin-bottom:.45rem}
label{display:block;font-weight:700;font-size:.9rem;margin:0 0 .35rem}
input[type=text],input[type=number],input[type=search],select,textarea,pre{width:100%;font:13px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;border:1px solid #d1d5db;border-radius:10px;padding:.7rem;background:#fff}
textarea,pre{min-height:10rem;resize:vertical}
pre{overflow:auto;background:#0b1220;color:#a7f3d0;margin:0;white-space:pre-wrap;word-break:break-all}
.row{display:flex;flex-wrap:wrap;gap:.5rem;align-items:center;margin:.6rem 0}
button,.btn{appearance:none;border:0;border-radius:9px;padding:.55rem .9rem;font-weight:700;cursor:pointer;text-decoration:none;display:inline-block;font-size:.92rem}
button.primary{background:#111827;color:#fff}button.secondary{background:#fff;color:#111827;border:1px solid #d1d5db}
.btn.green{background:#22c55e;color:#052e16}.btn.ghost{background:#fff;color:#111827;border:1px solid #d1d5db}
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem;margin:1rem 0}
.muted{color:#6b7280;font-size:.88rem}
.stat{font-size:.85rem;color:#065f46;background:#ecfdf5;border:1px solid #a7f3d0;border-radius:8px;padding:.45rem .65rem}
.err{display:none;background:#fef2f2;border:1px solid #fecaca;color:#991b1b;border-radius:8px;padding:.55rem .7rem;margin:.5rem 0;font-size:.9rem}
footer{color:#6b7280;font-size:.85rem;margin-top:1.4rem;border-top:1px solid #e5e7eb;padding-top:.9rem}
.opts{display:grid;gap:.55rem;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));margin:.5rem 0}
.opt{background:#f9fafb;border:1px solid #e5e7eb;border-radius:10px;padding:.55rem .7rem}
.opt label{margin:0 0 .25rem;font-size:.8rem}
.opt select,.opt input{padding:.4rem .5rem;font-size:.85rem}
.checks{display:flex;flex-wrap:wrap;gap:.55rem .9rem;margin:.35rem 0}
.checks label{font-weight:600;font-size:.88rem;display:flex;gap:.4rem;align-items:center;margin:0;cursor:pointer}
.mode{display:flex;flex-wrap:wrap;gap:.45rem;margin:.4rem 0}
.chip{appearance:none;border:1px solid #d1d5db;background:#fff;border-radius:999px;padding:.28rem .75rem;font-size:.8rem;font-weight:700;cursor:pointer;color:#374151}
.chip.on{background:#111827;color:#fff;border-color:#111827}
.meter{height:.55rem;background:#e5e7eb;border-radius:999px;overflow:hidden;margin:.45rem 0}
.meter > span{display:block;height:100%;width:0;background:#22c55e;transition:width .15s,background .15s}
.kv{display:grid;gap:.35rem;margin-top:.45rem}
.kv div{display:grid;grid-template-columns:9rem 1fr;gap:.5rem;font-size:.88rem}
.kv .k{color:#6b7280;font-weight:700}.kv .v{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;word-break:break-word}
.hint{color:#6b7280;font-size:.78rem;margin:.2rem 0 0;font-weight:500}
.single{font:800 1.35rem/1.35 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.03em;word-break:break-all;background:#0b1220;color:#a7f3d0;border-radius:10px;padding:.85rem .95rem;border:1px solid #1f2937;min-height:3.2rem}
.pill{display:inline-block;font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;border-radius:999px;padding:.12rem .5rem;background:#e5e7eb;color:#374151}
.pill.weak{background:#fee2e2;color:#991b1b}.pill.fair{background:#ffedd5;color:#9a3412}
.pill.good{background:#fef3c7;color:#92400e}.pill.strong{background:#d1fae5;color:#065f46}.pill.great{background:#dbeafe;color:#1e3a8a}
</style>
</head>
<body>
<div class="wrap">
<p class="muted" style="margin:.2rem 0"><a href="../">← Veriton Micro-Dev</a> · free tool · <a href="./">hub</a> · <a href="uuid-ulid.html">UUID/ULID</a> · <a href="hash.html">Hash</a> · <a href="base64-url.html">Base64/URL</a></p>
<div class="hero">
<span class="badge">FREE · browser-only · crypto RNG</span>
<h1>Password Generator</h1>
<p>Mint strong passwords or short passphrases in your tab. Charset toggles, ambiguous-char exclusion, batch export, and a rough entropy estimate. Uses <code>crypto.getRandomValues</code> — nothing is uploaded.</p>
<p style="margin:.8rem 0 0"><img src="../assets/password-gen-og.jpg" alt="Free password generator" width="640" height="360" style="max-width:100%;height:auto;border-radius:10px;border:1px solid #334155"/></p>
<p><a class="btn green" href="mailto:acer-openclaw@agentmail.to?subject=Veriton%20order%3A%20Code%20Review%20Sprint%20%241">Need auth/secret-handling review? $1 Review →</a>
<a class="btn ghost" href="uuid-ulid.html">UUID/ULID →</a>
<a class="btn ghost" href="hash.html">Hash/Checksum →</a></p>
</div>
<div class="card">
<div class="mode" role="tablist" aria-label="Generator mode">
<button type="button" class="chip on" id="modePass" data-mode="password">Password</button>
<button type="button" class="chip" id="modePhrase" data-mode="passphrase">Passphrase</button>
</div>
<div id="passwordOpts" class="opts">
<div class="opt">
<label for="len">Length</label>
<input id="len" type="number" min="4" max="256" value="20"/>
</div>
<div class="opt">
<label for="count">Count</label>
<input id="count" type="number" min="1" max="200" value="8"/>
</div>
<div class="opt" style="grid-column:1/-1">
<label>Character sets</label>
<div class="checks">
<label><input type="checkbox" id="useLower" checked/> lowercase a–z</label>
<label><input type="checkbox" id="useUpper" checked/> UPPERCASE A–Z</label>
<label><input type="checkbox" id="useDigits" checked/> digits 0–9</label>
<label><input type="checkbox" id="useSymbols" checked/> symbols !@#$…</label>
<label><input type="checkbox" id="excludeAmb" checked/> exclude ambiguous (0OIl1|`'\"…)</label>
<label><input type="checkbox" id="requireEach" checked/> require ≥1 from each selected set</label>
</div>
<p class="hint">Symbols default: <code>!@#$%^&*()-_=+[]{};:,.?/</code></p>
</div>
</div>
<div id="phraseOpts" class="opts" style="display:none">
<div class="opt">
<label for="words">Words</label>
<input id="words" type="number" min="3" max="12" value="5"/>
</div>
<div class="opt">
<label for="sep">Separator</label>
<select id="sep">
<option value="-" selected>hyphen -</option>
<option value=".">dot .</option>
<option value="_">underscore _</option>
<option value=" ">space</option>
<option value="">none</option>
</select>
</div>
<div class="opt">
<label for="cap">Capitalization</label>
<select id="cap">
<option value="lower" selected>lowercase</option>
<option value="title">Title Case</option>
<option value="upper">UPPERCASE</option>
<option value="alt">AlTeRnAtE words</option>
</select>
</div>
<div class="opt">
<label for="pcount">Count</label>
<input id="pcount" type="number" min="1" max="100" value="6"/>
</div>
<div class="opt">
<label for="digitTail">Digit tail</label>
<select id="digitTail">
<option value="0">none</option>
<option value="2" selected>2 digits</option>
<option value="3">3 digits</option>
<option value="4">4 digits</option>
</select>
</div>
</div>
<div class="row">
<button type="button" class="primary" id="gen">Generate</button>
<button type="button" class="secondary" id="copyOne">Copy first</button>
<button type="button" class="secondary" id="copyAll">Copy all</button>
<button type="button" class="secondary" id="download">Download .txt</button>
<span class="stat" id="stat">idle</span>
</div>
<div class="err" id="err"></div>
</div>
<div class="card">
<label>Primary result <span class="pill" id="strengthPill">—</span></label>
<div class="single" id="primary" aria-live="polite">Click Generate</div>
<div class="meter" aria-hidden="true"><span id="meterBar"></span></div>
<div class="kv" id="detail"></div>
</div>
<div class="card">
<label for="out">Batch output</label>
<pre id="out"># Generate passwords or passphrases</pre>
</div>
<div class="card">
<h2 style="margin-top:0;font-size:1.05rem">Why this exists</h2>
<p class="muted" style="margin:.2rem 0">Agents and indies constantly need throwaway secrets for local fixtures, CI smoke, and bootstrap docs. A local generator beats pasting entropy into random websites. Entropy here is a <strong>rough pool-size estimate</strong>, not a formal security audit. For real secret-handling review (leak paths, rotation, storage), that is the paid <strong>$1 Code Review</strong> SKU.</p>
</div>
<footer>
Veriton · <a href="https://github.com/veriton-dev/veriton-micro-dev">source</a> ·
<a href="./">all free tools</a> ·
contact <a href="mailto:acer-openclaw@agentmail.to">acer-openclaw@agentmail.to</a> ·
updated 2026-09-07
</footer>
</div>
<script>
(function () {
const LOWER = "abcdefghijklmnopqrstuvwxyz";
const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS = "0123456789";
const SYMBOLS = "!@#$%^&*()-_=+[]{};:,.?/";
const AMBIG = new Set("0OIl1|`'\".,;:{}[]()<>\\/");
// Compact EFF-inspired short word list (256 words = 8 bits each)
const WORDS = [
"able","acid","acre","aged","also","amid","area","army","atom","aunt","auto","away","axis","baby","back","bake",
"ball","band","bank","bare","barn","base","bath","bead","beam","bean","bear","beat","beef","bell","belt","bend",
"best","bike","bind","bird","bite","blow","blue","boat","body","boil","bold","bolt","bomb","bond","bone","book",
"boom","born","both","bowl","brag","brew","brow","bulk","burn","bush","busy","cage","cake","calf","call","calm",
"came","camp","card","care","cart","case","cash","cast","cave","cell","chat","chef","chew","chin","chip","city",
"clam","clap","claw","clay","clip","club","clue","coal","coat","code","coil","coin","cold","comb","come","cook",
"cool","cope","copy","cord","core","cork","corn","cost","cozy","crab","crew","crop","crow","cube","cult","curb",
"cure","curl","cute","damp","dare","dark","dart","dash","data","date","dawn","deal","dear","deck","deep","deny",
"desk","dial","dice","diet","dime","dirt","dish","dock","doll","dome","done","door","dose","down","draw","drip",
"drop","drum","dual","duck","dude","duel","duke","dune","dusk","dust","duty","each","earn","ease","east","easy",
"edge","edit","else","emit","envy","epic","even","ever","evil","exam","exit","face","fact","fade","fail","fair",
"fake","fall","fame","fang","farm","fast","fate","fear","feed","feel","fern","file","fill","film","find","fine",
"fire","firm","fish","fist","five","flag","flat","flaw","flea","flee","flip","flow","foam","fold","folk","font",
"food","fool","foot","ford","fork","form","fort","foul","four","free","frog","from","fuel","full","fund","fury",
"fuse","gain","game","gang","gate","gave","gear","gene","gift","girl","give","glad","glow","glue","goal","goat",
"gold","golf","gone","good","grab","gray","grew","grid","grim","grin","grip","grit","grow","gulf","guru","hail"
];
const els = {
modePass: document.getElementById("modePass"),
modePhrase: document.getElementById("modePhrase"),
passwordOpts: document.getElementById("passwordOpts"),
phraseOpts: document.getElementById("phraseOpts"),
len: document.getElementById("len"),
count: document.getElementById("count"),
useLower: document.getElementById("useLower"),
useUpper: document.getElementById("useUpper"),
useDigits: document.getElementById("useDigits"),
useSymbols: document.getElementById("useSymbols"),
excludeAmb: document.getElementById("excludeAmb"),
requireEach: document.getElementById("requireEach"),
words: document.getElementById("words"),
sep: document.getElementById("sep"),
cap: document.getElementById("cap"),
pcount: document.getElementById("pcount"),
digitTail: document.getElementById("digitTail"),
gen: document.getElementById("gen"),
copyOne: document.getElementById("copyOne"),
copyAll: document.getElementById("copyAll"),
download: document.getElementById("download"),
stat: document.getElementById("stat"),
err: document.getElementById("err"),
primary: document.getElementById("primary"),
out: document.getElementById("out"),
strengthPill: document.getElementById("strengthPill"),
meterBar: document.getElementById("meterBar"),
detail: document.getElementById("detail")
};
let mode = "password";
let lastBatch = [];
let lastMeta = null;
function showErr(msg) {
if (!msg) { els.err.style.display = "none"; els.err.textContent = ""; return; }
els.err.style.display = "block"; els.err.textContent = msg;
}
function setStat(t) { els.stat.textContent = t; }
function randBytes(n) {
const a = new Uint8Array(n);
if (window.crypto && crypto.getRandomValues) crypto.getRandomValues(a);
else for (let i = 0; i < n; i++) a[i] = Math.floor(Math.random() * 256) & 255;
return a;
}
// unbiased pick from alphabet via rejection sampling
function pickIndex(mod) {
if (mod <= 0) throw new Error("empty alphabet");
const max = 256 - (256 % mod);
while (true) {
const b = randBytes(1)[0];
if (b < max) return b % mod;
}
}
function pickChar(alphabet) {
return alphabet[pickIndex(alphabet.length)];
}
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = pickIndex(i + 1);
const t = arr[i]; arr[i] = arr[j]; arr[j] = t;
}
return arr;
}
function filterAmb(s) {
if (!els.excludeAmb.checked) return s;
let out = "";
for (const ch of s) if (!AMBIG.has(ch)) out += ch;
return out;
}
function buildAlphabet() {
const sets = [];
if (els.useLower.checked) sets.push(filterAmb(LOWER));
if (els.useUpper.checked) sets.push(filterAmb(UPPER));
if (els.useDigits.checked) sets.push(filterAmb(DIGITS));
if (els.useSymbols.checked) sets.push(filterAmb(SYMBOLS));
const nonempty = sets.filter(s => s.length > 0);
const pool = nonempty.join("");
// unique pool
let uniq = "";
const seen = new Set();
for (const ch of pool) {
if (!seen.has(ch)) { seen.add(ch); uniq += ch; }
}
return { sets: nonempty, pool: uniq };
}
function genPassword(len) {
const { sets, pool } = buildAlphabet();
if (!pool) throw new Error("Select at least one non-empty character set.");
if (len < 4 || len > 256) throw new Error("Length must be 4–256.");
const requireEach = els.requireEach.checked && sets.length > 1;
if (requireEach && len < sets.length) {
throw new Error("Length must be ≥ number of selected sets when requiring each set.");
}
const chars = [];
if (requireEach) {
for (const set of sets) chars.push(pickChar(set));
}
while (chars.length < len) chars.push(pickChar(pool));
shuffle(chars);
const value = chars.join("");
const bits = Math.log2(pool.length) * len;
return { value, poolSize: pool.length, bits, sets: sets.length };
}
function titleWord(w) { return w.charAt(0).toUpperCase() + w.slice(1); }
function genPassphrase(nWords) {
if (nWords < 3 || nWords > 12) throw new Error("Words must be 3–12.");
const sep = els.sep.value;
const cap = els.cap.value;
const digitN = parseInt(els.digitTail.value, 10) || 0;
const picked = [];
for (let i = 0; i < nWords; i++) {
let w = WORDS[pickIndex(WORDS.length)];
if (cap === "title") w = titleWord(w);
else if (cap === "upper") w = w.toUpperCase();
else if (cap === "alt") w = (i % 2 === 0) ? titleWord(w) : w;
picked.push(w);
}
let value = picked.join(sep);
if (digitN > 0) {
let d = "";
for (let i = 0; i < digitN; i++) d += String(pickIndex(10));
value += (sep || "") + d;
}
// entropy: log2(wordlist)^n + digit bits
let bits = Math.log2(WORDS.length) * nWords;
if (digitN > 0) bits += Math.log2(10) * digitN;
return { value, poolSize: WORDS.length, bits, words: nWords, digitN };
}
function strengthLabel(bits) {
if (bits < 40) return { label: "weak", cls: "weak", pct: 18 };
if (bits < 60) return { label: "fair", cls: "fair", pct: 38 };
if (bits < 80) return { label: "good", cls: "good", pct: 58 };
if (bits < 100) return { label: "strong", cls: "strong", pct: 78 };
return { label: "great", cls: "great", pct: 96 };
}
function renderMeta(meta) {
lastMeta = meta;
const s = strengthLabel(meta.bits);
els.strengthPill.textContent = s.label + " · ~" + meta.bits.toFixed(1) + " bits";
els.strengthPill.className = "pill " + s.cls;
els.meterBar.style.width = s.pct + "%";
els.meterBar.style.background =
s.cls === "weak" ? "#ef4444" :
s.cls === "fair" ? "#f97316" :
s.cls === "good" ? "#eab308" :
s.cls === "strong" ? "#22c55e" : "#3b82f6";
const rows = [
["mode", meta.mode],
["entropy_est", "~" + meta.bits.toFixed(2) + " bits (pool model)"],
["strength", s.label]
];
if (meta.mode === "password") {
rows.push(["length", String(meta.length)]);
rows.push(["pool_size", String(meta.poolSize)]);
rows.push(["sets", String(meta.sets)]);
} else {
rows.push(["words", String(meta.words)]);
rows.push(["wordlist", WORDS.length + " words (~8 bits each)"]);
if (meta.digitN) rows.push(["digit_tail", String(meta.digitN)]);
}
rows.push(["rng", (window.crypto && crypto.getRandomValues) ? "crypto.getRandomValues" : "Math.random fallback"]);
els.detail.innerHTML = rows.map(([k, v]) =>
`<div><span class="k">${k}</span><span class="v">${v}</span></div>`
).join("");
}
function setMode(next) {
mode = next;
els.modePass.classList.toggle("on", mode === "password");
els.modePhrase.classList.toggle("on", mode === "passphrase");
els.passwordOpts.style.display = mode === "password" ? "" : "none";
els.phraseOpts.style.display = mode === "passphrase" ? "" : "none";
}
function generate() {
showErr("");
try {
const batch = [];
let meta = null;
if (mode === "password") {
const len = parseInt(els.len.value, 10);
const count = parseInt(els.count.value, 10);
if (!(count >= 1 && count <= 200)) throw new Error("Count must be 1–200.");
for (let i = 0; i < count; i++) {
const r = genPassword(len);
batch.push(r.value);
if (!meta) meta = { mode: "password", bits: r.bits, poolSize: r.poolSize, sets: r.sets, length: len };
}
} else {
const nWords = parseInt(els.words.value, 10);
const count = parseInt(els.pcount.value, 10);
if (!(count >= 1 && count <= 100)) throw new Error("Count must be 1–100.");
for (let i = 0; i < count; i++) {
const r = genPassphrase(nWords);
batch.push(r.value);
if (!meta) meta = { mode: "passphrase", bits: r.bits, poolSize: r.poolSize, words: r.words, digitN: r.digitN };
}
}
lastBatch = batch;
els.primary.textContent = batch[0] || "";
els.out.textContent = batch.join("\n");
renderMeta(meta);
setStat(batch.length + " generated · ~" + meta.bits.toFixed(1) + " bits each");
} catch (e) {
showErr(e.message || String(e));
setStat("error");
}
}
async function copyText(t) {
if (!t) return false;
try {
await navigator.clipboard.writeText(t);
return true;
} catch (_) {
const ta = document.createElement("textarea");
ta.value = t; document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); return true; }
catch (_) { return false; }
finally { document.body.removeChild(ta); }
}
}
els.modePass.addEventListener("click", () => setMode("password"));
els.modePhrase.addEventListener("click", () => setMode("passphrase"));
els.gen.addEventListener("click", generate);
els.copyOne.addEventListener("click", async () => {
const ok = await copyText(lastBatch[0] || els.primary.textContent);
setStat(ok ? "copied first" : "copy failed");
});
els.copyAll.addEventListener("click", async () => {
const ok = await copyText(lastBatch.join("\n") || els.out.textContent);
setStat(ok ? "copied all" : "copy failed");
});
els.download.addEventListener("click", () => {
const body = lastBatch.join("\n") || els.out.textContent;
const blob = new Blob([body + "\n"], { type: "text/plain" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = mode === "password" ? "passwords.txt" : "passphrases.txt";
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1500);
setStat("download started");
});
// auto-generate on load
generate();
})();
</script>
</body>
</html>