Generator.tsx

6.9 KB · raw

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}