1import { createSignal, onMount, Show } from "solid-js";2import { generatePassword } from "../../shared/generate";3import type { PasswordPolicy } from "../../shared/types";4import { sendMessage, triggerPipPulse, triggerRing } from "../App";56interface GeneratorDrawerProps {7 open: boolean;8 policy?: PasswordPolicy;9 onUse: (password: string) => void;10 11 compact?: boolean;12}1314export function GeneratorDrawer(props: GeneratorDrawerProps) {15 const [password, setPassword] = createSignal("");16 const [length, setLength] = createSignal(20);17 const [upper, setUpper] = createSignal(true);18 const [lower, setLower] = createSignal(true);19 const [numbers, setNumbers] = createSignal(true);20 const [symbols, setSymbols] = createSignal(true);21 const [forbiddenChars, setForbiddenChars] = createSignal("");22 const [copied, setCopied] = createSignal(false);2324 function initFromPolicy(policy: PasswordPolicy) {25 let nextLength = length();26 let nextSymbols = symbols();2728 if (policy.minLength) nextLength = Math.max(nextLength, policy.minLength);29 if (policy.maxLength) nextLength = Math.min(nextLength, policy.maxLength);30 if (policy.forbidsSymbols) nextSymbols = false;31 if (policy.requiresSymbols) nextSymbols = true;3233 setLength(nextLength);34 setSymbols(nextSymbols);35 setForbiddenChars(policy.forbiddenChars ?? "");36 }3738 onMount(async () => {39 if (props.policy) {40 initFromPolicy(props.policy);41 } else {42 const res = await sendMessage({ type: "GET_SITE_PASSWORD_POLICY" });43 const data = (res as { ok: boolean; data?: PasswordPolicy }).data;44 if (data && Object.keys(data).length > 0) {45 initFromPolicy(data);46 }47 }48 regenerate();49 });5051 function regenerate(options?: {52 length?: number;53 upper?: boolean;54 lower?: boolean;55 numbers?: boolean;56 symbols?: boolean;57 }) {58 setPassword(59 generatePassword({60 length: options?.length ?? length(),61 includeUppercase: options?.upper ?? upper(),62 includeLowercase: options?.lower ?? lower(),63 includeNumbers: options?.numbers ?? numbers(),64 includeSymbols: options?.symbols ?? symbols(),65 forbiddenChars: forbiddenChars(),66 })67 );68 }6970 async function copyPassword(button?: HTMLButtonElement) {71 const response = await sendMessage({72 type: "COPY",73 payload: { text: password(), label: "password" },74 });75 if ((response as { ok: boolean }).ok) {76 triggerRing();77 triggerPipPulse();78 setCopied(true);79 if (button) {80 button.classList.remove("is-retreat");81 const drain = document.createElement("span");82 drain.className = "peach-copy-drain";83 const bar = document.createElement("span");84 bar.className = "peach-copy-drain-bar";85 drain.appendChild(bar);86 button.appendChild(drain);87 window.setTimeout(() => {88 setCopied(false);89 button.classList.remove("is-copied");90 button.classList.add("is-retreat");91 drain.remove();92 window.setTimeout(() => button.classList.remove("is-retreat"), 160);93 }, 1400);94 return;95 }96 window.setTimeout(() => setCopied(false), 1400);97 }98 }99100 function toggleUppercase() {101 const next = !upper();102 setUpper(next);103 regenerate({ upper: next });104 }105106 function toggleLowercase() {107 const next = !lower();108 setLower(next);109 regenerate({ lower: next });110 }111112 function toggleNumbers() {113 const next = !numbers();114 setNumbers(next);115 regenerate({ numbers: next });116 }117118 function toggleSymbols() {119 const next = !symbols();120 setSymbols(next);121 regenerate({ symbols: next });122 }123124 const passwordFontSize = () => {125 const value = password();126 if (value.length > 48) return "16px";127 if (value.length > 24) return "20px";128 return "24px";129 };130131 const RefreshIcon = () => (132 <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">133 <path d="M11 2.5A5.5 5.5 0 1 0 12.5 7" />134 <path d="M9.5 2.5H11V1" />135 </svg>136 );137138 const CopyIcon = () => (139 <span class="peach-copy-icon">140 <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">141 <rect x="4" y="4" width="8" height="9" rx="1.5" />142 <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" />143 </svg>144 </span>145 );146147 return (148 <div class={`peach-generator-drawer ${props.open ? "is-open" : ""}`}>149 <div class="peach-generator-drawer-inner">150 <div class={`peach-generator-drawer-content${props.compact ? " peach-generator-drawer-content--compact" : ""}`}>151152 {}153 <Show when={props.compact}>154 <div class="peach-pwgen-compact-display-row">155 <span class="peach-pwgen-compact-output">{password()}</span>156 <button157 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"158 onClick={() => regenerate()}159 aria-label="Regenerate"160 >161 <RefreshIcon />162 </button>163 <button164 class={`peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn ${copied() ? "is-copied" : ""}`}165 onClick={(e) => copyPassword(e.currentTarget as HTMLButtonElement)}166 aria-label="Copy"167 >168 <CopyIcon /><span class="peach-copy-sheen" />169 </button>170 </div>171 <div class="peach-pwgen-compact-controls">172 <input173 type="range"174 class="peach-pwgen-slider peach-pwgen-compact-slider"175 min="8"176 max="128"177 value={length()}178 onInput={(e) => {179 const n = parseInt((e.target as HTMLInputElement).value, 10);180 setLength(n);181 regenerate({ length: n });182 }}183 />184 <span class="peach-pwgen-len-num">{length()}</span>185 <div class="peach-pwgen-compact-pills">186 <button class={`peach-pwgen-pill ${upper() ? "is-on" : ""}`} onClick={toggleUppercase} type="button">A-Z</button>187 <button class={`peach-pwgen-pill ${lower() ? "is-on" : ""}`} onClick={toggleLowercase} type="button">a-z</button>188 <button class={`peach-pwgen-pill ${numbers() ? "is-on" : ""}`} onClick={toggleNumbers} type="button">0-9</button>189 <button class={`peach-pwgen-pill ${symbols() ? "is-on" : ""}`} onClick={toggleSymbols} type="button">!@#</button>190 </div>191 <button192 class="peach-btn peach-btn--accent peach-btn--sm peach-pwgen-compact-use"193 onClick={() => props.onUse(password())}194 >195 use196 </button>197 </div>198 </Show>199200 {}201 <Show when={!props.compact}>202 <div class="peach-pwgen-display-row">203 <p204 class="peach-pwgen-output-display"205 style={{ "font-size": passwordFontSize() }}206 >207 {password()}208 </p>209 <div class="peach-pwgen-display-actions">210 <button211 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"212 onClick={() => regenerate()}213 aria-label="Regenerate password"214 >215 <RefreshIcon />216 </button>217 <button218 class={`peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn ${copied() ? "is-copied" : ""}`}219 onClick={(event) => copyPassword(event.currentTarget as HTMLButtonElement)}220 aria-label="Copy password"221 >222 <CopyIcon /><span class="peach-copy-sheen" />223 </button>224 </div>225 </div>226227 <div class="peach-pwgen-length-row">228 <div class="peach-pwgen-length-labels">229 <span class="peach-label">length</span>230 <span class="peach-pwgen-len-num">{length()}</span>231 </div>232 <input233 type="range"234 class="peach-pwgen-slider"235 min="8"236 max="128"237 value={length()}238 onInput={(event) => {239 const nextLength = parseInt(240 (event.target as HTMLInputElement).value,241 10242 );243 setLength(nextLength);244 regenerate({ length: nextLength });245 }}246 />247 </div>248249 <div class="peach-pwgen-charset-pills">250 <button class={`peach-pwgen-pill ${upper() ? "is-on" : ""}`} onClick={toggleUppercase} type="button">A-Z</button>251 <button class={`peach-pwgen-pill ${lower() ? "is-on" : ""}`} onClick={toggleLowercase} type="button">a-z</button>252 <button class={`peach-pwgen-pill ${numbers() ? "is-on" : ""}`} onClick={toggleNumbers} type="button">0-9</button>253 <button class={`peach-pwgen-pill ${symbols() ? "is-on" : ""}`} onClick={toggleSymbols} type="button">!@#</button>254 </div>255256 <button257 class="peach-btn peach-btn--accent peach-btn--sm peach-pwgen-use-btn"258 onClick={() => props.onUse(password())}259 >260 use this password261 </button>262 </Show>263264 </div>265 </div>266 </div>267 );268}