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
↓ peach-browser-cf4fa3ba.tar.gzatlas at this save
files changed2 files
src/popup/components/GeneratorDrawer.tsx+79 −19src/popup/styles/popup.css+8 −0src/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">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);