004b3227fc8e
feat: add password and passphrase generator modes (v5.1.2)
- time
- 2026-09-05 12:49 UTC · 1 month ago
- machine
- koh2
- agent
- gpt-6
- session
- Vault V4 password and passphrase support
- parent
- 1cb20a22a633
↓ peach-browser-004b3227.tar.gzatlas at this save
files changed9 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/content/inject.generator.test.ts+45 −0src/content/inject.ts+104 −21src/popup/components/GeneratorDrawer.test.ts+1 −1src/popup/components/GeneratorDrawer.tsx+106 −32src/popup/generator-policy.ts+1 −1src/popup/styles/popup.css+4 −0manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.1.1",4+ "version": "5.1.2",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.1.1",5+ "version": "5.1.2",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.1.1",3+ "version": "5.1.2",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/content/inject.generator.test.ts+45 −0
@@ -0,0 +1,45 @@1+// @vitest-environment jsdom2+3+import { beforeEach, describe, expect, it } from "vitest";4+import "./_setup";5+import { resetChromeMock } from "./_setup";6+import { createSmartGenerator } from "./inject";7+8+describe("inline password and passphrase generator", () => {9+ beforeEach(() => {10+ resetChromeMock();11+ document.body.innerHTML = "";12+ });13+14+ it("uses a 256-character password range and a five-word passphrase default", () => {15+ const generator = createSmartGenerator({ policy: {} });16+ document.body.appendChild(generator.root);17+18+ const slider = generator.root.querySelector<HTMLInputElement>('input[type="range"]')!;19+ expect(slider.max).toBe("256");20+21+ const passphrase = Array.from(generator.root.querySelectorAll("button"))22+ .find((button) => button.textContent === "passphrase")!;23+ passphrase.click();24+25+ expect(slider.min).toBe("4");26+ expect(slider.max).toBe("12");27+ expect(slider.value).toBe("5");28+ expect(slider.getAttribute("aria-label")).toBe("Word count");29+ expect(generator.getPassword().split("-")).toHaveLength(5);30+ });31+32+ it("generates twelve words with the selected separator", () => {33+ const generator = createSmartGenerator({ policy: {} });34+ document.body.appendChild(generator.root);35+ const buttons = Array.from(generator.root.querySelectorAll("button"));36+ buttons.find((button) => button.textContent === "passphrase")!.click();37+ buttons.find((button) => button.textContent === ".")!.click();38+39+ const slider = generator.root.querySelector<HTMLInputElement>('input[type="range"]')!;40+ slider.value = "12";41+ slider.dispatchEvent(new Event("input"));42+43+ expect(generator.getPassword().split(".")).toHaveLength(12);44+ });45+});src/content/inject.ts+104 −21
@@ -10,7 +10,14 @@1010 */1111 1212 import { ACCENT_COLORS, type PasswordPolicy, type SavedIdentity } from "../shared/types";13-import { generatePassword } from "../shared/generate";13+import {14+ DEFAULT_PASSPHRASE_WORDS,15+ generatePassphrase,16+ generatePassword,17+ MAX_PASSPHRASE_WORDS,18+ MIN_PASSPHRASE_WORDS,19+ type PassphraseSeparator,20+} from "../shared/generate";1421 import { detectPasswordPolicy } from "./policy";1522 import {1623 filterSavedIdentities,@@ -300,14 +307,17 @@300307 * page. Writing this in pure DOM rather than reusing the Solid component301308 * keeps the content-script bundle small.302309 */303-function createSmartGenerator(opts: SmartGeneratorOptions): SmartGenerator {310+export function createSmartGenerator(opts: SmartGeneratorOptions): SmartGenerator {304311 const policy = opts.policy ?? {};312+ let mode: "password" | "passphrase" = "password";313+ let wordCount = DEFAULT_PASSPHRASE_WORDS;314+ let separator: PassphraseSeparator = "-";305315 306316 // Start at site max (strongest password the site accepts), fall back to 64307317 let length = policy.maxLength ?? 64;308318 if (policy.minLength) length = Math.max(length, policy.minLength);309319 if (length < 8) length = 8;310- if (length > 128) length = 128;320+ if (length > 256) length = 256;311321 312322 let upper = true;313323 let lower = true;@@ -321,6 +331,20 @@321331 const root = document.createElement("div");322332 root.className = "pwgen-block";323333 334+ const modePills = root.appendChild(document.createElement("div"));335+ modePills.className = "pwgen-pills";336+ modePills.setAttribute("aria-label", "Generator type");337+ const passwordModeBtn = modePills.appendChild(document.createElement("button"));338+ passwordModeBtn.type = "button";339+ passwordModeBtn.className = "pwgen-pill is-on";340+ passwordModeBtn.textContent = "password";341+ passwordModeBtn.setAttribute("aria-pressed", "true");342+ const passphraseModeBtn = modePills.appendChild(document.createElement("button"));343+ passphraseModeBtn.type = "button";344+ passphraseModeBtn.className = "pwgen-pill";345+ passphraseModeBtn.textContent = "passphrase";346+ passphraseModeBtn.setAttribute("aria-pressed", "false");347+324348 const displayRow = root.appendChild(document.createElement("div"));325349 displayRow.className = "pwgen-display-row";326350 const output = displayRow.appendChild(document.createElement("input")) as HTMLInputElement;@@ -351,10 +375,14 @@351375 slider.type = "range";352376 slider.className = "pwgen-slider";353377 slider.min = "8";354- slider.max = "128";378+ slider.max = "256";355379 slider.value = String(length);356380 if (policy.minLength) slider.min = String(Math.max(8, policy.minLength));357- if (policy.maxLength) slider.max = String(Math.min(128, policy.maxLength));381+ if (policy.maxLength) slider.max = String(Math.min(256, policy.maxLength));382+ slider.setAttribute("aria-label", "Password length");383+ const amountLabel = controls.appendChild(document.createElement("span"));384+ amountLabel.className = "pwgen-amount-label";385+ amountLabel.textContent = "characters";358386 const lenNum = controls.appendChild(document.createElement("span"));359387 lenNum.className = "pwgen-len-num pwgen-len-num--edit";360388 lenNum.textContent = String(length);@@ -367,11 +395,13 @@367395 function commitLenEdit() {368396 const n = parseInt(li.value, 10);369397 if (!Number.isNaN(n)) {370- length = Math.max(parseInt(slider.min, 10), Math.min(parseInt(slider.max, 10), n));371- slider.value = String(length);398+ const clamped = Math.max(parseInt(slider.min, 10), Math.min(parseInt(slider.max, 10), n));399+ if (mode === "passphrase") wordCount = clamped;400+ else length = clamped;401+ slider.value = String(mode === "passphrase" ? wordCount : length);372402 regenerate();373403 }374- lenNum.textContent = String(length);404+ lenNum.textContent = String(mode === "passphrase" ? wordCount : length);375405 li.replaceWith(lenNum);376406 }377407 li.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); commitLenEdit(); } else if (e.key === "Escape") { li.replaceWith(lenNum); } });@@ -402,6 +432,27 @@402432 regenerate();403433 }));404434 435+ const separatorPills = root.appendChild(document.createElement("div"));436+ separatorPills.className = "pwgen-pills";437+ separatorPills.hidden = true;438+ for (const value of ["-", " ", ".", "_"] as PassphraseSeparator[]) {439+ const button = separatorPills.appendChild(document.createElement("button"));440+ button.type = "button";441+ button.className = `pwgen-pill ${value === separator ? "is-on" : ""}`;442+ button.textContent = value === " " ? "space" : value;443+ button.setAttribute("aria-label", value === " " ? "Space separator" : `${value} separator`);444+ button.setAttribute("aria-pressed", String(value === separator));445+ button.addEventListener("click", () => {446+ separator = value;447+ for (const sibling of separatorPills.querySelectorAll("button")) {448+ const selected = sibling === button;449+ sibling.classList.toggle("is-on", selected);450+ sibling.setAttribute("aria-pressed", String(selected));451+ }452+ regenerate();453+ });454+ }455+405456 // Policy hint — surfaces what Peach read off the site so the user knows the406457 // generator is already tuned to it. Only shows when something interesting407458 // was detected.@@ -411,8 +462,10 @@411462 if (policy.requiresSymbols) hintParts.push("requires symbols");412463 if (policy.forbidsSymbols) hintParts.push("no symbols");413464 if (policy.forbiddenChars) hintParts.push(`excludes ${policy.forbiddenChars.slice(0, 6)}${policy.forbiddenChars.length > 6 ? "…" : ""}`);465+ let policyHint: HTMLDivElement | null = null;414466 if (hintParts.length > 0) {415467 const hint = root.appendChild(document.createElement("div"));468+ policyHint = hint;416469 hint.className = "pwgen-policy-hint";417470 const accent = document.createElement("span");418471 accent.className = "pwgen-hint-accent";@@ -425,33 +478,59 @@425478 function regenerate() {426479 // Falls back gracefully when the user toggles every charset off — the427480 // shared generator forces lowercase in that case rather than throwing.428- password = generatePassword({429- length,430- includeUppercase: upper,431- includeLowercase: lower,432- includeNumbers: numbers,433- includeSymbols: symbols,434- forbiddenChars,435- });481+ password = mode === "passphrase"482+ ? generatePassphrase({ wordCount, separator })483+ : generatePassword({484+ length,485+ includeUppercase: upper,486+ includeLowercase: lower,487+ includeNumbers: numbers,488+ includeSymbols: symbols,489+ forbiddenChars,490+ });436491 output.value = password;437492 opts.onChange?.(password);438493 }439494 440495 output.addEventListener("input", () => {441496 password = output.value;442- length = password.length;443- slider.value = String(Math.max(8, Math.min(128, length)));444- lenNum.textContent = String(length);497+ if (mode === "password") {498+ length = password.length;499+ slider.value = String(Math.max(8, Math.min(256, length)));500+ lenNum.textContent = String(length);501+ }445502 opts.onChange?.(password);446503 });447504 448505 slider.addEventListener("input", () => {449- length = parseInt(slider.value, 10);450- lenNum.textContent = String(length);506+ if (mode === "passphrase") wordCount = parseInt(slider.value, 10);507+ else length = parseInt(slider.value, 10);508+ lenNum.textContent = String(mode === "passphrase" ? wordCount : length);451509 regenerate();452510 });453511 regenBtn.addEventListener("click", regenerate);454512 513+ function selectMode(nextMode: "password" | "passphrase") {514+ mode = nextMode;515+ const isPassphrase = mode === "passphrase";516+ passwordModeBtn.classList.toggle("is-on", !isPassphrase);517+ passwordModeBtn.setAttribute("aria-pressed", String(!isPassphrase));518+ passphraseModeBtn.classList.toggle("is-on", isPassphrase);519+ passphraseModeBtn.setAttribute("aria-pressed", String(isPassphrase));520+ pills.hidden = isPassphrase;521+ separatorPills.hidden = !isPassphrase;522+ if (policyHint) policyHint.hidden = isPassphrase;523+ slider.min = String(isPassphrase ? MIN_PASSPHRASE_WORDS : Math.max(8, policy.minLength ?? 8));524+ slider.max = String(isPassphrase ? MAX_PASSPHRASE_WORDS : Math.min(256, policy.maxLength ?? 256));525+ slider.value = String(isPassphrase ? wordCount : length);526+ slider.setAttribute("aria-label", isPassphrase ? "Word count" : "Password length");527+ amountLabel.textContent = isPassphrase ? "words" : "characters";528+ lenNum.textContent = String(isPassphrase ? wordCount : length);529+ regenerate();530+ }531+ passwordModeBtn.addEventListener("click", () => selectMode("password"));532+ passphraseModeBtn.addEventListener("click", () => selectMode("passphrase"));533+455534 let copyResetTimer: ReturnType<typeof setTimeout> | null = null;456535 async function copy(): Promise<void> {457536 try {@@ -1299,6 +1378,10 @@12991378 .pwgen-len-num {13001379 font-family: "IBM Plex Mono", monospace; font-size: 11px;13011380 color: var(--peach-accent); min-width: 22px; text-align: right;1381+ }1382+ .pwgen-amount-label {1383+ color: var(--peach-text-muted); font-family: "IBM Plex Mono", monospace;1384+ font-size: 9px; min-width: 50px;13021385 }13031386 .pwgen-len-num--edit { cursor: text; border-bottom: 1px dotted color-mix(in srgb, var(--peach-accent) 50%, transparent); }13041387 .pwgen-len-input {src/popup/components/GeneratorDrawer.test.ts+1 −1
@@ -22,7 +22,7 @@2222 it('leaves slider bounds at defaults', () => {2323 const r = initFromPolicy(null, { currentLength: 64, userHasAdjusted: false });2424 expect(r.sliderMin).toBe(8);25- expect(r.sliderMax).toBe(128);25+ expect(r.sliderMax).toBe(256);2626 });2727 });2828 src/popup/components/GeneratorDrawer.tsx+106 −32
@@ -1,5 +1,12 @@11 import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js";2-import { generatePassword } from "../../shared/generate";2+import {3+ DEFAULT_PASSPHRASE_WORDS,4+ generatePassphrase,5+ generatePassword,6+ MAX_PASSPHRASE_WORDS,7+ MIN_PASSPHRASE_WORDS,8+ type PassphraseSeparator,9+} from "../../shared/generate";310 import type { PasswordPolicy } from "../../shared/types";411 import { sendMessage, triggerPipPulse, triggerRing } from "../App";512 import { popupCopy } from "../copy";@@ -15,8 +22,11 @@1522 }1623 1724 export function GeneratorDrawer(props: GeneratorDrawerProps) {25+ const [mode, setMode] = createSignal<"password" | "passphrase">("password");1826 const [password, setPassword] = createSignal("");1927 const [length, setLength] = createSignal(64);28+ const [wordCount, setWordCount] = createSignal(DEFAULT_PASSPHRASE_WORDS);29+ const [separator, setSeparator] = createSignal<PassphraseSeparator>("-");2030 const [upper, setUpper] = createSignal(true);2131 const [lower, setLower] = createSignal(true);2232 const [numbers, setNumbers] = createSignal(true);@@ -27,7 +37,7 @@2737 const [lengthDraft, setLengthDraft] = createSignal("");2838 const [userHasAdjusted, setUserHasAdjusted] = createSignal(false);2939 const [sliderMin, setSliderMin] = createSignal(8);30- const [sliderMax, setSliderMax] = createSignal(128);40+ const [sliderMax, setSliderMax] = createSignal(256);3141 const [sitePolicy, setSitePolicy] = createSignal<PasswordPolicy | null>(null);3242 3343 function initFromPolicy(policy: PasswordPolicy) {@@ -35,7 +45,7 @@3545 3646 // Update slider bounds to reflect site constraints (cases 5, 6)3747 if (policy.minLength) setSliderMin(Math.max(1, policy.minLength));38- if (policy.maxLength) setSliderMax(Math.min(128, policy.maxLength));48+ if (policy.maxLength) setSliderMax(Math.min(256, policy.maxLength));3949 4050 const currentLength = length();4151 @@ -84,6 +94,7 @@8494 });8595 8696 const isConstrained = createMemo(() => {97+ if (mode() === "passphrase") return false;8798 const policy = sitePolicy();8899 if (!policy) return false;89100 const len = length();@@ -92,6 +103,7 @@92103 });93104 94105 const constraintNote = createMemo(() => {106+ if (mode() === "passphrase") return "";95107 const policy = sitePolicy();96108 if (!policy) return "";97109 const len = length();@@ -105,26 +117,40 @@105117 });106118 107119 function handleLengthChange(n: number) {120+ if (mode() === "passphrase") {121+ setWordCount(n);122+ regenerate({ wordCount: n });123+ return;124+ }108125 setUserHasAdjusted(true);109126 setLength(n);110127 regenerate({ length: n });111128 }112129 113130 function regenerate(options?: {131+ mode?: "password" | "passphrase";114132 length?: number;133+ wordCount?: number;134+ separator?: PassphraseSeparator;115135 upper?: boolean;116136 lower?: boolean;117137 numbers?: boolean;118138 symbols?: boolean;119139 }) {120- const pw = generatePassword({121- length: options?.length ?? length(),122- includeUppercase: options?.upper ?? upper(),123- includeLowercase: options?.lower ?? lower(),124- includeNumbers: options?.numbers ?? numbers(),125- includeSymbols: options?.symbols ?? symbols(),126- forbiddenChars: forbiddenChars(),127- });140+ const nextMode = options?.mode ?? mode();141+ const pw = nextMode === "passphrase"142+ ? generatePassphrase({143+ wordCount: options?.wordCount ?? wordCount(),144+ separator: options?.separator ?? separator(),145+ })146+ : generatePassword({147+ length: options?.length ?? length(),148+ includeUppercase: options?.upper ?? upper(),149+ includeLowercase: options?.lower ?? lower(),150+ includeNumbers: options?.numbers ?? numbers(),151+ includeSymbols: options?.symbols ?? symbols(),152+ forbiddenChars: forbiddenChars(),153+ });128154 setPassword(pw);129155 props.onChange?.(pw);130156 }@@ -181,14 +207,16 @@181207 }182208 183209 function startEditingLength() {184- setLengthDraft(String(length()));210+ setLengthDraft(String(mode() === "passphrase" ? wordCount() : length()));185211 setEditingLength(true);186212 }187213 188214 function commitLength() {189215 const n = parseInt(lengthDraft(), 10);190216 if (!Number.isNaN(n)) {191- const clamped = Math.max(sliderMin(), Math.min(sliderMax(), n));217+ const min = mode() === "passphrase" ? MIN_PASSPHRASE_WORDS : sliderMin();218+ const max = mode() === "passphrase" ? MAX_PASSPHRASE_WORDS : sliderMax();219+ const clamped = Math.max(min, Math.min(max, n));192220 handleLengthChange(clamped);193221 }194222 setEditingLength(false);@@ -201,6 +229,54 @@201229 return "24px";202230 };203231 232+ function selectMode(nextMode: "password" | "passphrase") {233+ setMode(nextMode);234+ setEditingLength(false);235+ regenerate({ mode: nextMode });236+ }237+238+ function selectSeparator(nextSeparator: PassphraseSeparator) {239+ setSeparator(nextSeparator);240+ regenerate({ separator: nextSeparator });241+ }242+243+ const currentAmount = () => mode() === "passphrase" ? wordCount() : length();244+ const currentMin = () => mode() === "passphrase" ? MIN_PASSPHRASE_WORDS : sliderMin();245+ const currentMax = () => mode() === "passphrase" ? MAX_PASSPHRASE_WORDS : sliderMax();246+247+ const ModeControls = () => (248+ <div class="peach-pwgen-mode-pills" aria-label="Generator type">249+ <button class={`peach-pwgen-pill ${mode() === "password" ? "is-on" : ""}`} aria-pressed={mode() === "password"} onClick={() => selectMode("password")} type="button">password</button>250+ <button class={`peach-pwgen-pill ${mode() === "passphrase" ? "is-on" : ""}`} aria-pressed={mode() === "passphrase"} onClick={() => selectMode("passphrase")} type="button">passphrase</button>251+ </div>252+ );253+254+ const OptionPills = () => (255+ <Show256+ when={mode() === "passphrase"}257+ fallback={258+ <>259+ <button class={`peach-pwgen-pill ${upper() ? "is-on" : ""}`} aria-pressed={upper()} onClick={toggleUppercase} type="button">A-Z</button>260+ <button class={`peach-pwgen-pill ${lower() ? "is-on" : ""}`} aria-pressed={lower()} onClick={toggleLowercase} type="button">a-z</button>261+ <button class={`peach-pwgen-pill ${numbers() ? "is-on" : ""}`} aria-pressed={numbers()} onClick={toggleNumbers} type="button">0-9</button>262+ <button class={`peach-pwgen-pill ${symbols() ? "is-on" : ""}`} aria-pressed={symbols()} onClick={toggleSymbols} type="button">!@#</button>263+ </>264+ }265+ >266+ {(["-", " ", ".", "_"] as PassphraseSeparator[]).map((value) => (267+ <button268+ class={`peach-pwgen-pill ${separator() === value ? "is-on" : ""}`}269+ aria-label={value === " " ? "Space separator" : `${value} separator`}270+ aria-pressed={separator() === value}271+ onClick={() => selectSeparator(value)}272+ type="button"273+ >274+ {value === " " ? "space" : value}275+ </button>276+ ))}277+ </Show>278+ );279+204280 const RefreshIcon = () => (205281 <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">206282 <path d="M11 2.5A5.5 5.5 0 1 0 12.5 7" />@@ -224,6 +300,7 @@224300 225301 {/* ── Compact layout ──────────────────────────────── */}226302 <Show when={props.compact}>303+ <ModeControls />227304 <div class="peach-pwgen-compact-display-row">228305 <input229306 class="peach-pwgen-compact-output"@@ -234,7 +311,7 @@234311 onInput={(e) => {235312 const pw = (e.currentTarget as HTMLInputElement).value;236313 setPassword(pw);237- setLength(pw.length);314+ if (mode() === "password") setLength(pw.length);238315 props.onChange?.(pw);239316 }}240317 aria-label="Generated password — click to edit"@@ -258,9 +335,10 @@258335 <input259336 type="range"260337 class="peach-pwgen-slider peach-pwgen-compact-slider"261- min={sliderMin()}262- max={sliderMax()}263- value={length()}338+ min={currentMin()}339+ max={currentMax()}340+ value={currentAmount()}341+ aria-label={mode() === "passphrase" ? "Word count" : "Password length"}264342 onInput={(e) => {265343 const n = parseInt((e.target as HTMLInputElement).value, 10);266344 handleLengthChange(n);@@ -269,7 +347,7 @@269347 <Show270348 when={editingLength()}271349 fallback={272- <span class="peach-pwgen-len-num peach-pwgen-len-num--edit" onClick={startEditingLength}>{length()}</span>350+ <span class="peach-pwgen-len-num peach-pwgen-len-num--edit" onClick={startEditingLength}>{currentAmount()}</span>273351 }274352 >275353 <input@@ -284,10 +362,7 @@284362 />285363 </Show>286364 <div class="peach-pwgen-compact-pills">287- <button class={`peach-pwgen-pill ${upper() ? "is-on" : ""}`} onClick={toggleUppercase} type="button">A-Z</button>288- <button class={`peach-pwgen-pill ${lower() ? "is-on" : ""}`} onClick={toggleLowercase} type="button">a-z</button>289- <button class={`peach-pwgen-pill ${numbers() ? "is-on" : ""}`} onClick={toggleNumbers} type="button">0-9</button>290- <button class={`peach-pwgen-pill ${symbols() ? "is-on" : ""}`} onClick={toggleSymbols} type="button">!@#</button>365+ <OptionPills />291366 </div>292367 <button293368 class="peach-btn peach-btn--accent peach-btn--sm peach-pwgen-compact-use"@@ -303,6 +378,7 @@303378 304379 {/* ── Full layout ─────────────────────────────────── */}305380 <Show when={!props.compact}>381+ <ModeControls />306382 <div class="peach-pwgen-display-row">307383 <p308384 class="peach-pwgen-output-display"@@ -330,11 +406,11 @@330406 331407 <div class="peach-pwgen-length-row">332408 <div class="peach-pwgen-length-labels">333- <span class="peach-label">length</span>409+ <span class="peach-label">{mode() === "passphrase" ? "word count" : "length"}</span>334410 <Show335411 when={editingLength()}336412 fallback={337- <span class="peach-pwgen-len-num peach-pwgen-len-num--edit" onClick={startEditingLength}>{length()}</span>413+ <span class="peach-pwgen-len-num peach-pwgen-len-num--edit" onClick={startEditingLength}>{currentAmount()}</span>338414 }339415 >340416 <input@@ -352,9 +428,10 @@352428 <input353429 type="range"354430 class="peach-pwgen-slider"355- min={sliderMin()}356- max={sliderMax()}357- value={length()}431+ min={currentMin()}432+ max={currentMax()}433+ value={currentAmount()}434+ aria-label={mode() === "passphrase" ? "Word count" : "Password length"}358435 onInput={(event) => {359436 const n = parseInt(360437 (event.target as HTMLInputElement).value,@@ -369,17 +446,14 @@369446 </div>370447 371448 <div class="peach-pwgen-charset-pills">372- <button class={`peach-pwgen-pill ${upper() ? "is-on" : ""}`} onClick={toggleUppercase} type="button">A-Z</button>373- <button class={`peach-pwgen-pill ${lower() ? "is-on" : ""}`} onClick={toggleLowercase} type="button">a-z</button>374- <button class={`peach-pwgen-pill ${numbers() ? "is-on" : ""}`} onClick={toggleNumbers} type="button">0-9</button>375- <button class={`peach-pwgen-pill ${symbols() ? "is-on" : ""}`} onClick={toggleSymbols} type="button">!@#</button>449+ <OptionPills />376450 </div>377451 378452 <button379453 class="peach-btn peach-btn--accent peach-btn--sm peach-pwgen-use-btn"380454 onClick={() => props.onUse(password())}381455 >382- use this password456+ use this {mode()}383457 </button>384458 </Show>385459 src/popup/generator-policy.ts+1 −1
@@ -14,7 +14,7 @@1414 1515 const DEFAULT_LENGTH = 64;1616 const DEFAULT_SLIDER_MIN = 8;17-const DEFAULT_SLIDER_MAX = 128;17+const DEFAULT_SLIDER_MAX = 256;1818 1919 export function initGeneratorState(): GeneratorState {2020 return { currentLength: DEFAULT_LENGTH, userHasAdjusted: false };src/popup/styles/popup.css+4 −0
@@ -2730,6 +2730,10 @@27302730 gap: 6px;27312731 flex-wrap: wrap;27322732 }2733+.peach-pwgen-mode-pills {2734+ display: flex;2735+ gap: 5px;2736+}27332737 .peach-pwgen-pill {27342738 padding: 4px 11px;27352739 border-radius: 999px;