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
9 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 −0
manifest.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": {

view file

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",

view file

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",

view file

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

view file

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 {

view file

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 

view file

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 

view file

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

view file

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;

view file