1import { createSignal, onMount, Show } from "solid-js";2import { generatePassword } from "../../shared/generate";3import type { PasswordPolicy } from "../../shared/types";4import { sendMessage, triggerPipPulse, triggerRing } from "../App";5import { popupCopy } from "../copy";67interface GeneratorDrawerProps {8 open: boolean;9 policy?: PasswordPolicy;10 onUse: (password: string) => void;11 12 compact?: boolean;13}1415export function GeneratorDrawer(props: GeneratorDrawerProps) {16 const [password, setPassword] = createSignal("");17 const [length, setLength] = createSignal(20);18 const [upper, setUpper] = createSignal(true);19 const [lower, setLower] = createSignal(true);20 const [numbers, setNumbers] = createSignal(true);21 const [symbols, setSymbols] = createSignal(true);22 const [forbiddenChars, setForbiddenChars] = createSignal("");23 const [copied, setCopied] = createSignal(false);2425 function initFromPolicy(policy: PasswordPolicy) {26 let nextLength = length();27 let nextSymbols = symbols();2829 if (policy.minLength) nextLength = Math.max(nextLength, policy.minLength);30 if (policy.maxLength) nextLength = Math.min(nextLength, policy.maxLength);31 if (policy.forbidsSymbols) nextSymbols = false;32 if (policy.requiresSymbols) nextSymbols = true;3334 setLength(nextLength);35 setSymbols(nextSymbols);36 setForbiddenChars(policy.forbiddenChars ?? "");37 }3839 onMount(async () => {40 if (props.policy) {41 initFromPolicy(props.policy);42 } else {43 const res = await sendMessage({ type: "GET_SITE_PASSWORD_POLICY" });44 const data = (res as { ok: boolean; data?: PasswordPolicy }).data;45 if (data && Object.keys(data).length > 0) {46 initFromPolicy(data);47 }48 }49 regenerate();50 });5152 function regenerate(options?: {53 length?: number;54 upper?: boolean;55 lower?: boolean;56 numbers?: boolean;57 symbols?: boolean;58 }) {59 setPassword(60 generatePassword({61 length: options?.length ?? length(),62 includeUppercase: options?.upper ?? upper(),63 includeLowercase: options?.lower ?? lower(),64 includeNumbers: options?.numbers ?? numbers(),65 includeSymbols: options?.symbols ?? symbols(),66 forbiddenChars: forbiddenChars(),67 })68 );69 }7071 async function copyPassword(button?: HTMLButtonElement) {72 const ok = await popupCopy(password(), "password");73 if (ok) {74 triggerRing();75 triggerPipPulse();76 setCopied(true);77 if (button) {78 button.classList.remove("is-retreat");79 const drain = document.createElement("span");80 drain.className = "peach-copy-drain";81 const bar = document.createElement("span");82 bar.className = "peach-copy-drain-bar";83 drain.appendChild(bar);84 button.appendChild(drain);85 window.setTimeout(() => {86 setCopied(false);87 button.classList.remove("is-copied");88 button.classList.add("is-retreat");89 drain.remove();90 window.setTimeout(() => button.classList.remove("is-retreat"), 160);91 }, 1400);92 return;93 }94 window.setTimeout(() => setCopied(false), 1400);95 }96 }9798 function toggleUppercase() {99 const next = !upper();100 setUpper(next);101 regenerate({ upper: next });102 }103104 function toggleLowercase() {105 const next = !lower();106 setLower(next);107 regenerate({ lower: next });108 }109110 function toggleNumbers() {111 const next = !numbers();112 setNumbers(next);113 regenerate({ numbers: next });114 }115116 function toggleSymbols() {117 const next = !symbols();118 setSymbols(next);119 regenerate({ symbols: next });120 }121122 const passwordFontSize = () => {123 const value = password();124 if (value.length > 48) return "16px";125 if (value.length > 24) return "20px";126 return "24px";127 };128129 const RefreshIcon = () => (130 <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">131 <path d="M11 2.5A5.5 5.5 0 1 0 12.5 7" />132 <path d="M9.5 2.5H11V1" />133 </svg>134 );135136 const CopyIcon = () => (137 <span class="peach-copy-icon">138 <svg class="peach-copy-icon-svg" width="12" height="12" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">139 <rect x="4" y="4" width="8" height="9" rx="1.5" />140 <path d="M10 4V2.5A1.5 1.5 0 0 0 8.5 1h-6A1.5 1.5 0 0 0 1 2.5v7A1.5 1.5 0 0 0 2.5 11H4" />141 </svg>142 </span>143 );144145 return (146 <div class={`peach-generator-drawer ${props.open ? "is-open" : ""}`}>147 <div class="peach-generator-drawer-inner">148 <div class={`peach-generator-drawer-content${props.compact ? " peach-generator-drawer-content--compact" : ""}`}>149150 {}151 <Show when={props.compact}>152 <div class="peach-pwgen-compact-display-row">153 <span class="peach-pwgen-compact-output">{password()}</span>154 <button155 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"156 onClick={() => regenerate()}157 aria-label="Regenerate"158 >159 <RefreshIcon />160 </button>161 <button162 class={`peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn ${copied() ? "is-copied" : ""}`}163 onClick={(e) => copyPassword(e.currentTarget as HTMLButtonElement)}164 aria-label="Copy"165 >166 <CopyIcon /><span class="peach-copy-sheen" />167 </button>168 </div>169 <div class="peach-pwgen-compact-controls">170 <input171 type="range"172 class="peach-pwgen-slider peach-pwgen-compact-slider"173 min="8"174 max="128"175 value={length()}176 onInput={(e) => {177 const n = parseInt((e.target as HTMLInputElement).value, 10);178 setLength(n);179 regenerate({ length: n });180 }}181 />182 <span class="peach-pwgen-len-num">{length()}</span>183 <div class="peach-pwgen-compact-pills">184 <button class={`peach-pwgen-pill ${upper() ? "is-on" : ""}`} onClick={toggleUppercase} type="button">A-Z</button>185 <button class={`peach-pwgen-pill ${lower() ? "is-on" : ""}`} onClick={toggleLowercase} type="button">a-z</button>186 <button class={`peach-pwgen-pill ${numbers() ? "is-on" : ""}`} onClick={toggleNumbers} type="button">0-9</button>187 <button class={`peach-pwgen-pill ${symbols() ? "is-on" : ""}`} onClick={toggleSymbols} type="button">!@#</button>188 </div>189 <button190 class="peach-btn peach-btn--accent peach-btn--sm peach-pwgen-compact-use"191 onClick={() => props.onUse(password())}192 >193 use194 </button>195 </div>196 </Show>197198 {}199 <Show when={!props.compact}>200 <div class="peach-pwgen-display-row">201 <p202 class="peach-pwgen-output-display"203 style={{ "font-size": passwordFontSize() }}204 >205 {password()}206 </p>207 <div class="peach-pwgen-display-actions">208 <button209 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"210 onClick={() => regenerate()}211 aria-label="Regenerate password"212 >213 <RefreshIcon />214 </button>215 <button216 class={`peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn ${copied() ? "is-copied" : ""}`}217 onClick={(event) => copyPassword(event.currentTarget as HTMLButtonElement)}218 aria-label="Copy password"219 >220 <CopyIcon /><span class="peach-copy-sheen" />221 </button>222 </div>223 </div>224225 <div class="peach-pwgen-length-row">226 <div class="peach-pwgen-length-labels">227 <span class="peach-label">length</span>228 <span class="peach-pwgen-len-num">{length()}</span>229 </div>230 <input231 type="range"232 class="peach-pwgen-slider"233 min="8"234 max="128"235 value={length()}236 onInput={(event) => {237 const nextLength = parseInt(238 (event.target as HTMLInputElement).value,239 10240 );241 setLength(nextLength);242 regenerate({ length: nextLength });243 }}244 />245 </div>246247 <div class="peach-pwgen-charset-pills">248 <button class={`peach-pwgen-pill ${upper() ? "is-on" : ""}`} onClick={toggleUppercase} type="button">A-Z</button>249 <button class={`peach-pwgen-pill ${lower() ? "is-on" : ""}`} onClick={toggleLowercase} type="button">a-z</button>250 <button class={`peach-pwgen-pill ${numbers() ? "is-on" : ""}`} onClick={toggleNumbers} type="button">0-9</button>251 <button class={`peach-pwgen-pill ${symbols() ? "is-on" : ""}`} onClick={toggleSymbols} type="button">!@#</button>252 </div>253254 <button255 class="peach-btn peach-btn--accent peach-btn--sm peach-pwgen-use-btn"256 onClick={() => props.onUse(password())}257 >258 use this password259 </button>260 </Show>261262 </div>263 </div>264 </div>265 );266}