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 GeneratorProps {7 onBack: () => void;8 onUse: (password: string) => void;9 policy?: PasswordPolicy;10}1112export function GeneratorView(props: GeneratorProps) {13 const [password, setPassword] = createSignal("");14 const [length, setLength] = createSignal(24);15 const [upper, setUpper] = createSignal(true);16 const [lower, setLower] = createSignal(true);17 const [numbers, setNumbers] = createSignal(true);18 const [symbols, setSymbols] = createSignal(true);19 const [copied, setCopied] = createSignal(false);20 const [detectedPolicy, setDetectedPolicy] = createSignal<PasswordPolicy | null>(null);2122 function initFromPolicy(policy: PasswordPolicy) {23 if (policy.minLength) setLength(Math.max(length(), policy.minLength));24 if (policy.maxLength) setLength(Math.min(length(), policy.maxLength));25 if (policy.forbidsSymbols) setSymbols(false);26 if (policy.requiresSymbols) setSymbols(true);27 }2829 onMount(async () => {30 if (props.policy) {31 setDetectedPolicy(props.policy);32 initFromPolicy(props.policy);33 } else {34 const res = await sendMessage({ type: "GET_SITE_PASSWORD_POLICY" });35 const data = (res as { ok: boolean; data?: PasswordPolicy }).data;36 if (data && Object.keys(data).length > 0) {37 setDetectedPolicy(data);38 initFromPolicy(data);39 }40 }41 regenerate();42 });4344 function regenerate(nextLength = length()) {45 setPassword(46 generatePassword({47 length: nextLength,48 includeUppercase: upper(),49 includeLowercase: lower(),50 includeNumbers: numbers(),51 includeSymbols: symbols(),52 })53 );54 }5556 async function copyPassword(button?: HTMLButtonElement) {57 const response = await sendMessage({58 type: "COPY",59 payload: { text: password(), label: "password" },60 });61 if ((response as { ok: boolean }).ok) {62 triggerRing();63 triggerPipPulse();64 setCopied(true);65 if (button) {66 button.classList.remove("is-retreat");67 const drain = document.createElement("span");68 drain.className = "peach-copy-drain";69 const bar = document.createElement("span");70 bar.className = "peach-copy-drain-bar";71 drain.appendChild(bar);72 button.appendChild(drain);73 window.setTimeout(() => {74 setCopied(false);75 button.classList.remove("is-copied");76 button.classList.add("is-retreat");77 drain.remove();78 window.setTimeout(() => button.classList.remove("is-retreat"), 160);79 }, 1400);80 return;81 }82 window.setTimeout(() => setCopied(false), 1400);83 }84 }8586 return (87 <div class="peach-overlay-view ov-enter-right" style="display:flex">88 <div class="peach-overlay-header">89 <button90 class="peach-btn peach-btn--subtle peach-btn--sm peach-overlay-action"91 onClick={props.onBack}92 >93 cancel94 </button>95 <span class="peach-overlay-title">password generator</span>96 <button97 class="peach-btn peach-btn--accent peach-btn--sm peach-overlay-action"98 onClick={() => props.onUse(password())}99 >100 use101 </button>102 </div>103104 <div class="peach-pwgen-body">105 <Show when={detectedPolicy()}>106 <div class="peach-policy-badge">107 <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1" style="margin-right:4px;opacity:0.7;">108 <circle cx="5" cy="5" r="4" />109 <path d="M5 3v3M5 7h.01" />110 </svg>111 site requirements detected112 </div>113 </Show>114 <div class="peach-pwgen-display">115 <p class="peach-pwgen-output">{password()}</p>116 <div class="peach-pwgen-display-actions">117 <button118 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"119 onClick={() => regenerate()}120 aria-label="Regenerate"121 >122 <svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">123 <path d="M11 2.5A5.5 5.5 0 1 0 12.5 7" />124 <path d="M9.5 2.5H11V1" />125 </svg>126 </button>127 <button128 class={`peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn peach-copy-btn ${copied() ? "is-copied" : ""}`}129 onClick={(e) =>130 void copyPassword(e.currentTarget as HTMLButtonElement)131 }132 aria-label="Copy password"133 >134 <span class="peach-copy-icon">135 <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">136 <rect x="4" y="4" width="8" height="9" rx="1.5" />137 <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" />138 </svg>139 </span>140 <span class="peach-copy-sheen"></span>141 </button>142 </div>143 </div>144145 <div class="peach-pwgen-settings">146 <div class="peach-pwgen-length-row">147 <div class="peach-pwgen-length-labels">148 <span class="peach-label">length</span>149 <span class="peach-pwgen-len-num">{length()}</span>150 </div>151 <input152 class="peach-pwgen-slider"153 type="range"154 min="8"155 max="128"156 value={length()}157 onInput={(e) => {158 const nextLength = parseInt((e.currentTarget as HTMLInputElement).value, 10);159 setLength(nextLength);160 regenerate(nextLength);161 }}162 />163 </div>164165 <div class="peach-pwgen-toggles">166 {[167 ["Uppercase A-Z", upper, setUpper],168 ["Lowercase a-z", lower, setLower],169 ["Numbers 0-9", numbers, setNumbers],170 ["Symbols !@#$", symbols, setSymbols],171 ].map(([label, getter, setter]) => (172 <div class="peach-pwgen-toggle-row">173 <span class="peach-pwgen-toggle-label">{label as string}</span>174 <button175 class={`peach-toggle ${(getter as () => boolean)() ? "is-on" : ""}`}176 onClick={() => {177 (setter as (value: boolean) => void)(!(getter as () => boolean)());178 regenerate();179 }}180 >181 <span class="peach-toggle-knob"></span>182 </button>183 </div>184 ))}185 </div>186 </div>187 </div>188 </div>189 );190}