cf4fa3ba4eac

implement site policy constraint behavior: snap to max on fresh open, respect user adjustments, enforce min/max bounds, show constraint note

time
2026-05-15 01:25 UTC · 5 months ago
machine
koh2
parent
915845b2a0ad
2 files src/popup/components/GeneratorDrawer.tsx+79 −19src/popup/styles/popup.css+8 −0
src/popup/components/GeneratorDrawer.tsx+79 −19
@@ -1,4 +1,4 @@1-import { createSignal, onMount, Show } from "solid-js";1+import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js";22 import { generatePassword } from "../../shared/generate";33 import type { PasswordPolicy } from "../../shared/types";44 import { sendMessage, triggerPipPulse, triggerRing } from "../App";@@ -25,21 +25,51 @@2525   const [copied, setCopied] = createSignal(false);2626   const [editingLength, setEditingLength] = createSignal(false);2727   const [lengthDraft, setLengthDraft] = createSignal("");28+  const [userHasAdjusted, setUserHasAdjusted] = createSignal(false);29+  const [sliderMin, setSliderMin] = createSignal(8);30+  const [sliderMax, setSliderMax] = createSignal(128);31+  const [sitePolicy, setSitePolicy] = createSignal<PasswordPolicy | null>(null);2832 2933   function initFromPolicy(policy: PasswordPolicy) {30-    let nextLength = length();31-    let nextSymbols = symbols();34+    setSitePolicy(policy);3235 33-    if (policy.minLength) nextLength = Math.max(nextLength, policy.minLength);34-    if (policy.maxLength) nextLength = Math.min(nextLength, policy.maxLength);36+    // Update slider bounds to reflect site constraints (cases 5, 6)37+    if (policy.minLength) setSliderMin(Math.max(1, policy.minLength));38+    if (policy.maxLength) setSliderMax(Math.min(128, policy.maxLength));39+40+    const currentLength = length();41+42+    if (!userHasAdjusted()) {43+      // Case 2: fresh open — snap to site max (strongest password the site accepts)44+      const target = policy.maxLength ?? currentLength;45+      // Case 5/6: raise to min if max is somehow below it46+      setLength(policy.minLength ? Math.max(target, policy.minLength) : target);47+    } else {48+      // Case 3: user set higher than site max — snap down49+      if (policy.maxLength && currentLength > policy.maxLength) {50+        setLength(policy.maxLength);51+      }52+      // Case 5: user set lower than site min — snap up53+      if (policy.minLength && currentLength < policy.minLength) {54+        setLength(policy.minLength);55+      }56+      // Case 4: within bounds — keep user's value (no-op)57+    }58+59+    let nextSymbols = symbols();3560     if (policy.forbidsSymbols) nextSymbols = false;3661     if (policy.requiresSymbols) nextSymbols = true;37-38-    setLength(nextLength);3962     setSymbols(nextSymbols);4063     setForbiddenChars(policy.forbiddenChars ?? "");4164   }4265 66+  // Reset adjustment flag each time the drawer opens fresh67+  createEffect<boolean>((wasOpen) => {68+    const isOpen = props.open;69+    if (!wasOpen && isOpen) setUserHasAdjusted(false);70+    return isOpen;71+  }, false);72+4373   onMount(async () => {4474     if (props.policy) {4575       initFromPolicy(props.policy);@@ -53,6 +83,33 @@5383     regenerate();5484   });5585 86+  const isConstrained = createMemo(() => {87+    const policy = sitePolicy();88+    if (!policy) return false;89+    const len = length();90+    return (!!policy.maxLength && len >= policy.maxLength) ||91+           (!!policy.minLength && len <= policy.minLength);92+  });93+94+  const constraintNote = createMemo(() => {95+    const policy = sitePolicy();96+    if (!policy) return "";97+    const len = length();98+    if (policy.maxLength && len >= policy.maxLength) {99+      return `This site allows max ${policy.maxLength} characters`;100+    }101+    if (policy.minLength && len <= policy.minLength) {102+      return `This site requires at least ${policy.minLength} characters`;103+    }104+    return "";105+  });106+107+  function handleLengthChange(n: number) {108+    setUserHasAdjusted(true);109+    setLength(n);110+    regenerate({ length: n });111+  }112+56113   function regenerate(options?: {57114     length?: number;58115     upper?: boolean;@@ -131,9 +188,8 @@131188   function commitLength() {132189     const n = parseInt(lengthDraft(), 10);133190     if (!isNaN(n)) {134-      const clamped = Math.max(8, Math.min(128, n));135-      setLength(clamped);136-      regenerate({ length: clamped });191+      const clamped = Math.max(sliderMin(), Math.min(sliderMax(), n));192+      handleLengthChange(clamped);137193     }138194     setEditingLength(false);139195   }@@ -202,13 +258,12 @@202258               <input203259                 type="range"204260                 class="peach-pwgen-slider peach-pwgen-compact-slider"205-                min="8"206-                max="128"261+                min={sliderMin()}262+                max={sliderMax()}207263                 value={length()}208264                 onInput={(e) => {209265                   const n = parseInt((e.target as HTMLInputElement).value, 10);210-                  setLength(n);211-                  regenerate({ length: n });266+                  handleLengthChange(n);212267                 }}213268               />214269               <Show@@ -241,6 +296,9 @@241296                 use242297               </button>243298             </div>299+            <Show when={isConstrained()}>300+              <p class="peach-generator-constraint-note">{constraintNote()}</p>301+            </Show>244302           </Show>245303 246304           {/* ── Full layout ─────────────────────────────────── */}@@ -294,18 +352,20 @@294352               <input295353                 type="range"296354                 class="peach-pwgen-slider"297-                min="8"298-                max="128"355+                min={sliderMin()}356+                max={sliderMax()}299357                 value={length()}300358                 onInput={(event) => {301-                  const nextLength = parseInt(359+                  const n = parseInt(302360                     (event.target as HTMLInputElement).value,303361                     10304362                   );305-                  setLength(nextLength);306-                  regenerate({ length: nextLength });363+                  handleLengthChange(n);307364                 }}308365               />366+              <Show when={isConstrained()}>367+                <p class="peach-generator-constraint-note">{constraintNote()}</p>368+              </Show>309369             </div>310370 311371             <div class="peach-pwgen-charset-pills">

view file

src/popup/styles/popup.css+8 −0
@@ -3209,6 +3209,14 @@32093209   border-radius: 8px;32103210   margin-top: 2px;32113211 }3212+.peach-generator-constraint-note {3213+  font-family: 'IBM Plex Mono', monospace;3214+  font-size: 10px;3215+  color: var(--peach-t3);3216+  letter-spacing: 0.04em;3217+  margin: 0;3218+  transition: opacity 200ms ease;3219+}32123220 .peach-pwgen-toggle-row {32133221   display: flex; align-items: center; justify-content: space-between;32143222   padding: 6px 0; border-bottom: 1px solid var(--peach-t3);

view file