1import { createSignal, createMemo, createEffect, For, Show, onMount, onCleanup } from "solid-js";2import type { VaultEntry } from "../../shared/types";3import { sendMessage } from "../App";4import { parseOtpauthURI } from "../../crypto/totp";5import { NoteEditorInline, Toolbar } from "./NoteEditor";6import { GeneratorDrawer } from "../components/GeneratorDrawer";78interface AddProps {9 entry?: VaultEntry;10 entries?: VaultEntry[];11 autoOpenGenerator?: boolean;12 onBack: () => void;13 onSave: (entry?: VaultEntry) => void | Promise<void>;14}1516export function AddView(props: AddProps) {17 const [title, setTitle] = createSignal(props.entry?.title ?? "");18 const [username, setUsername] = createSignal(props.entry?.username ?? "");19 const [password, setPassword] = createSignal(props.entry?.password ?? "");20 const [url, setUrl] = createSignal(props.entry?.url ?? "");21 const [totp, setTotp] = createSignal(props.entry?.totp ?? "");22 const [notes, setNotes] = createSignal(props.entry?.notes ?? "");23 const [saving, setSaving] = createSignal(false);24 const [error, setError] = createSignal("");25 const [passwordStrength, setPasswordStrength] = createSignal<number | null>(null);26 const [zxcvbnFn, setZxcvbnFn] = createSignal<((pwd: string) => { score: number }) | null>(null);27 const [tags, setTags] = createSignal<string[]>(props.entry?.tags ?? []);28 const [tagInput, setTagInput] = createSignal("");29 const [showTagSuggestions, setShowTagSuggestions] = createSignal(false);30 const [showGenerator, setShowGenerator] = createSignal(props.autoOpenGenerator ?? false);31 const [showPassword, setShowPassword] = createSignal(false);32 const [showTotp, setShowTotp] = createSignal(!!(props.entry?.totp));33 const [showNotes, setShowNotes] = createSignal(!!(props.entry?.notes));34 const [email, setEmail] = createSignal((props.entry as VaultEntry & { email?: string })?.email ?? "");35 const [activeIdentityField, setActiveIdentityField] = createSignal<"username" | "email" | null>(null);36 const [currentTabUrl, setCurrentTabUrl] = createSignal("");37 const [identityFilter, setIdentityFilter] = createSignal<"all" | "username" | "email">("all");3839 const allIdentities = createMemo(() => {40 const emailMap = new Map<string, number>();41 const usernameMap = new Map<string, number>();42 for (const entry of props.entries ?? []) {43 const uname = entry.username?.trim() ?? "";44 const em = (entry as VaultEntry & { email?: string }).email?.trim() ?? "";45 if (uname) usernameMap.set(uname, (usernameMap.get(uname) ?? 0) + 1);46 if (em) emailMap.set(em, (emailMap.get(em) ?? 0) + 1);47 }48 const result: Array<{ value: string; count: number; type: "email" | "username" }> = [];49 const seen = new Set<string>();50 for (const v of new Set([...emailMap.keys(), ...usernameMap.keys()])) {51 if (seen.has(v)) continue;52 seen.add(v);53 const ec = emailMap.get(v) ?? 0;54 const uc = usernameMap.get(v) ?? 0;55 result.push({ value: v, count: Math.max(ec, uc), type: ec >= uc ? "email" : "username" });56 }57 return result.sort((a, b) => b.count - a.count);58 });5960 const identitySuggestions = createMemo(() => {61 const field = activeIdentityField();62 if (!field) return [];63 const input = (field === "username" ? username() : email()).toLowerCase();64 const filter = identityFilter();65 return allIdentities()66 .filter(s => (filter === "all" || s.type === filter) && (!input || s.value.toLowerCase().includes(input)))67 .slice(0, 8);68 });6970 const isNoteEntry = () => props.entry?.type === "note";71 const isCardEntry = () => props.entry?.type === "card";72 const [selectedType, setSelectedType] = createSignal<"login" | "note" | "card">(73 isNoteEntry() ? "note" : isCardEntry() ? "card" : "login"74 );7576 77 const [cardNickname, setCardNickname] = createSignal(props.entry?.cardNickname ?? "");78 const [cardholderName, setCardholderName] = createSignal(props.entry?.cardholderName ?? "");79 const [cardNumber, setCardNumber] = createSignal(props.entry?.cardNumber ?? "");80 const [expMonth, setExpMonth] = createSignal(props.entry?.expMonth ?? "");81 const [expYear, setExpYear] = createSignal(props.entry?.expYear ?? "");82 const [cvc, setCvc] = createSignal(props.entry?.cvc ?? "");83 const [showCvc, setShowCvc] = createSignal(false);84 const [cardNumberFocused, setCardNumberFocused] = createSignal(false);85 const [noteEditorEl, setNoteEditorEl] = createSignal<HTMLDivElement | null>(null);8687 function noteExec(cmd: string, val?: string) {88 document.execCommand(cmd, false, val);89 noteEditorEl()?.focus();90 }9192 function getCardNetwork(num: string): string {93 const n = num.replace(/\s/g, '');94 if (/^4/.test(n)) return 'Visa';95 if (/^5[1-5]|^2[2-7]/.test(n)) return 'Mastercard';96 if (/^3[47]/.test(n)) return 'Amex';97 if (/^6(?:011|5)/.test(n)) return 'Discover';98 return '';99 }100101 function formatCardNumberDisplay(raw: string, focused: boolean): string {102 const n = raw.replace(/\s/g, '');103 if (!focused && n.length >= 4) return '•••• •••• •••• ' + n.slice(-4);104 if (/^3[47]/.test(n)) return n.replace(/(\d{4})(\d{1,6})?(\d{1,5})?/, (_, a, b, c) => [a, b, c].filter(Boolean).join(' '));105 return n.replace(/(\d{4})(?=\d)/g, '$1 ').trim();106 }107108 const canSave = () => {109 if (!title().trim()) return false;110 if (selectedType() === "login" && !password().trim()) return false;111 if (selectedType() === "card" && (!cardholderName().trim() || !cardNumber().trim())) return false;112 return true;113 };114115 const allTags = createMemo(() => {116 const tagSet = new Set<string>();117 for (const entry of props.entries ?? []) {118 for (const tag of entry.tags ?? []) tagSet.add(tag);119 }120 return Array.from(tagSet).sort();121 });122123 const tagSuggestions = createMemo(() => {124 const input = tagInput().toLowerCase().trim();125 if (!input) return [];126 return allTags().filter(t =>127 t.toLowerCase().includes(input) && !tags().includes(t)128 ).slice(0, 5);129 });130131 function addTag(tag: string) {132 const n = tag.toLowerCase().trim();133 if (!n || n.length > 32 || tags().includes(n) || tags().length >= 10) return;134 setTags([...tags(), n]);135 }136137 function removeTag(tag: string) {138 setTags(tags().filter(t => t !== tag));139 }140141 function handleTagKeyDown(e: KeyboardEvent) {142 if (e.key === "Enter" || e.key === ",") {143 e.preventDefault();144 if (tagInput()) { addTag(tagInput()); setTagInput(""); setShowTagSuggestions(false); }145 } else if (e.key === "Backspace" && !tagInput() && tags().length > 0) {146 removeTag(tags()[tags().length - 1]);147 }148 }149150 151 createEffect(async () => {152 const pwd = password();153 if (pwd.length < 4) { setPasswordStrength(null); return; }154 try {155 if (!zxcvbnFn()) {156 const mod = await import("zxcvbn");157 setZxcvbnFn(() => mod.default);158 }159 const fn = zxcvbnFn();160 if (fn) setPasswordStrength(fn(pwd).score);161 } catch {162 setPasswordStrength(pwd.length >= 16 ? 3 : pwd.length >= 12 ? 2 : 1);163 }164 });165166 onMount(() => {167 const el = document.querySelector(".peach-new-title-input") as HTMLInputElement | null;168 if (el) el.focus();169 chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {170 const tabUrl = tabs[0]?.url;171 if (tabUrl && tabUrl.startsWith("http")) setCurrentTabUrl(tabUrl);172 });173 174 175 const keepAlive = setInterval(() => {176 chrome.runtime.sendMessage({ type: "TOUCH_AUTO_LOCK" }).catch(() => {});177 }, 30_000);178 onCleanup(() => clearInterval(keepAlive));179 });180181 function getNormalizedTotp(): string | undefined {182 const value = totp().trim();183 if (!value) return undefined;184 if (value.startsWith("otpauth://")) {185 const parsed = parseOtpauthURI(value);186 return parsed?.secret ?? value;187 }188 return value;189 }190191 async function handleSave() {192 if (!canSave()) {193 setError(194 selectedType() === "note"195 ? "title is required"196 : selectedType() === "card"197 ? "cardholder name and card number are required"198 : "title and password are required"199 );200 return;201 }202203 setSaving(true);204 setError("");205206 const payload: Record<string, unknown> = {207 type: selectedType(),208 title: title().trim(),209 notes: notes().trim() || undefined,210 tags: tags().length > 0 ? tags() : undefined,211 };212213 if (selectedType() === "login") {214 payload.username = username().trim();215 payload.email = email().trim() || undefined;216 payload.password = password();217 payload.url = url().trim();218 payload.totp = getNormalizedTotp();219 }220221 if (selectedType() === "card") {222 const raw = cardNumber().replace(/\s/g, '');223 payload.cardholderName = cardholderName().trim();224 payload.cardNumber = raw;225 payload.expMonth = expMonth().trim();226 payload.expYear = expYear().trim();227 payload.cvc = cvc().trim();228 payload.cardNickname = cardNickname().trim() || undefined;229 const n = raw;230 payload.network = /^4/.test(n) ? 'visa' : /^5[1-5]|^2[2-7]/.test(n) ? 'mastercard' : /^3[47]/.test(n) ? 'amex' : /^6(?:011|5)/.test(n) ? 'discover' : 'unknown';231 payload.last4 = raw.slice(-4);232 }233234 if (props.entry) {235 const res = await sendMessage({ type: "UPDATE_ENTRY", payload: { id: props.entry.id, patch: payload } });236 const result = res as { ok: boolean; error?: string };237 if (!result.ok) { setError(result.error ?? "could not update entry"); setSaving(false); return; }238 await props.onSave({ ...props.entry, ...payload } as VaultEntry);239 return;240 }241242 const res = await sendMessage({ type: "ADD_ENTRY", payload });243 const result = res as { ok: boolean; data?: VaultEntry; error?: string };244 if (!result.ok) {245 if (result.error === "vault locked") {246 setError("vault is locked — tap the lock icon to re-enter your master password");247 } else {248 setError(result.error ?? "could not save entry");249 }250 setSaving(false);251 return;252 }253 await props.onSave(result.data);254 }255256 const strengthLabel = () => {257 const s = passwordStrength();258 if (s === null) return null;259 return ["weak", "weak", "fair", "good", "strong"][s] as string;260 };261262 return (263 <div class="peach-overlay-view ov-enter-right" style="display:flex">264 <div class="peach-overlay-header">265 <button266 class="peach-btn peach-btn--ghost peach-btn--sm peach-overlay-action"267 onClick={props.onBack}268 >269 cancel270 </button>271 <span class="peach-overlay-title">272 {props.entry ? "edit entry" : "new entry"}273 </span>274 <button275 class="peach-btn peach-btn--accent peach-btn--sm peach-overlay-action"276 onClick={() => void handleSave()}277 disabled={saving() || !canSave()}278 >279 {saving() ? "saving…" : props.entry ? "save" : "create"}280 </button>281 </div>282283 {}284 <div class="peach-entry-body peach-entry-body--new">285286 {}287 <Show when={!props.entry}>288 <div class="peach-new-type-row">289 <button290 class={`peach-new-type-btn ${selectedType() === "login" ? "is-active" : ""}`}291 onClick={() => setSelectedType("login")}292 >login</button>293 <button294 class={`peach-new-type-btn ${selectedType() === "note" ? "is-active" : ""}`}295 onClick={() => setSelectedType("note")}296 >note</button>297 <button298 class={`peach-new-type-btn ${selectedType() === "card" ? "is-active" : ""}`}299 onClick={() => setSelectedType("card")}300 >card</button>301 </div>302 </Show>303304 {}305 <Show when={selectedType() === "note"}>306 <Toolbar exec={noteExec} class="peach-note-toolbar-body" />307 </Show>308309 {}310 <div class="peach-new-title-wrap">311 <input312 class="peach-new-title-input"313 placeholder={selectedType() === "note" ? "note title" : "entry name"}314 autocomplete="off"315 value={title()}316 onInput={(e) => setTitle((e.currentTarget as HTMLInputElement).value)}317 onKeyDown={(e) => { if (e.key === "Enter") void handleSave(); }}318 />319 </div>320321 {}322 <Show when={selectedType() === "login"}>323 <div class="peach-domain">324 <div class="peach-domain-edit-row">325 <input326 class="peach-domain-value peach-domain-value--edit"327 placeholder="domain.com"328 autocomplete="off"329 value={url()}330 onInput={(e) => setUrl((e.currentTarget as HTMLInputElement).value)}331 />332 <Show when={currentTabUrl()}>333 <button334 class="peach-domain-url-btn"335 type="button"336 onClick={() => setUrl(currentTabUrl())}337 title="Insert current page URL"338 >339 <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">340 <path d="M4.5 7.5a3 3 0 0 0 4.243 0L10.5 5.75a3 3 0 0 0-4.243-4.243L5.5 2.26" />341 <path d="M7.5 4.5a3 3 0 0 0-4.243 0L1.5 6.25a3 3 0 0 0 4.243 4.243L6.5 9.74" />342 </svg>343 </button>344 </Show>345 </div>346 </div>347348 <section class="peach-fields">349 {}350 <div class="peach-field-card">351 <span class="peach-label">username</span>352 <div class="peach-field-value-row">353 <input354 class="peach-field-value peach-field-value--edit"355 placeholder="johndoe"356 autocomplete="off"357 value={username()}358 onInput={(e) => setUsername((e.currentTarget as HTMLInputElement).value)}359 onFocus={() => setActiveIdentityField("username")}360 onBlur={() => setTimeout(() => setActiveIdentityField(null), 150)}361 />362 </div>363 <Show when={activeIdentityField() === "username" && allIdentities().length > 0}>364 <IdentitySuggestList365 suggestions={identitySuggestions()}366 filter={identityFilter()}367 onFilter={setIdentityFilter}368 onPick={(v) => { setUsername(v); setActiveIdentityField(null); }}369 />370 </Show>371 </div>372373 {}374 <div class="peach-field-card">375 <span class="peach-label">email</span>376 <div class="peach-field-value-row">377 <input378 class="peach-field-value peach-field-value--edit"379 type="email"380 placeholder="[email protected]"381 autocomplete="off"382 value={email()}383 onInput={(e) => setEmail((e.currentTarget as HTMLInputElement).value)}384 onFocus={() => setActiveIdentityField("email")}385 onBlur={() => setTimeout(() => setActiveIdentityField(null), 150)}386 />387 </div>388 <Show when={activeIdentityField() === "email" && allIdentities().length > 0}>389 <IdentitySuggestList390 suggestions={identitySuggestions()}391 filter={identityFilter()}392 onFilter={setIdentityFilter}393 onPick={(v) => { setEmail(v); setActiveIdentityField(null); }}394 />395 </Show>396 </div>397398 {}399 <div class="peach-field-card">400 <span class="peach-label">password</span>401 <div class="peach-password-edit-row">402 <input403 class="peach-field-value peach-field-value--edit"404 type={showPassword() ? "text" : "password"}405 placeholder="password"406 autocomplete="new-password"407 value={password()}408 onInput={(e) => setPassword((e.currentTarget as HTMLInputElement).value)}409 />410 <button411 class="peach-inline-action peach-password-toggle-btn"412 type="button"413 onClick={() => setShowPassword(!showPassword())}414 aria-label={showPassword() ? "Hide password" : "Show password"}415 >416 <Show417 when={showPassword()}418 fallback={419 <svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">420 <path d="M1 5C1 5 2.8 1 7 1C11.2 1 13 5 13 5C13 5 11.2 9 7 9C2.8 9 1 5 1 5Z" />421 <circle cx="7" cy="5" r="1.75" />422 </svg>423 }424 >425 <svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">426 <path d="M1 2C3 4.5 5 6 7 6C9 6 11 4.5 13 2" />427 <path d="M3.5 5.5L2.5 7.5" />428 <path d="M7 6.5V8.5" />429 <path d="M10.5 5.5L11.5 7.5" />430 </svg>431 </Show>432 </button>433 </div>434435 {}436 <Show when={strengthLabel() !== null}>437 <div class="peach-strength-hairline">438 <div class="peach-strength-hairline-track">439 <div440 class="peach-strength-hairline-fill"441 style={{442 width: ["25%","40%","60%","80%","100%"][passwordStrength()!],443 background: ["rgba(220,80,60,0.65)","rgba(220,80,60,0.65)","rgba(200,140,60,0.6)","rgba(180,162,100,0.55)","rgba(150,175,110,0.6)"][passwordStrength()!],444 }}445 />446 </div>447 <span class="peach-strength-label">{strengthLabel()}</span>448 </div>449 </Show>450451 {}452 <button453 class="peach-generate-toggle-link"454 type="button"455 onClick={() => { setShowGenerator(!showGenerator()); }}456 >457 {showGenerator() ? "☰ hide generator" : "⚙ generate"}458 </button>459 <GeneratorDrawer460 compact461 open={showGenerator()}462 onUse={(pw) => { setPassword(pw); setShowGenerator(false); }}463 />464 </div>465466 {}467 <Show when={showTotp() || totp()}>468 <div class="peach-field-card">469 <span class="peach-label">2fa secret</span>470 <div class="peach-field-value-row">471 <input472 class="peach-field-value peach-field-value--edit"473 placeholder="BASE32 or otpauth:// URI"474 autocomplete="off"475 value={totp()}476 onInput={(e) => setTotp((e.currentTarget as HTMLInputElement).value)}477 />478 </div>479 </div>480 </Show>481 </section>482 </Show>483484 {}485 <Show when={selectedType() === "card"}>486 <section class="peach-fields">487 {}488 <div class="peach-field-card">489 <span class="peach-label">nickname</span>490 <div class="peach-field-value-row">491 <input class="peach-field-value peach-field-value--edit" placeholder="Personal Visa"492 autocomplete="off" value={cardNickname()}493 onInput={(e) => setCardNickname((e.currentTarget as HTMLInputElement).value)} />494 </div>495 </div>496 {}497 <div class="peach-field-card">498 <span class="peach-label">cardholder name</span>499 <div class="peach-field-value-row">500 <input class="peach-field-value peach-field-value--edit" placeholder="Jane Smith"501 autocomplete="cc-name" value={cardholderName()}502 onInput={(e) => setCardholderName((e.currentTarget as HTMLInputElement).value)} />503 </div>504 </div>505 {}506 <div class="peach-field-card">507 <span class="peach-label">508 card number509 <Show when={getCardNetwork(cardNumber())}>510 <span class="peach-card-network-badge">{getCardNetwork(cardNumber())}</span>511 </Show>512 </span>513 <div class="peach-field-value-row">514 <input class="peach-field-value peach-field-value--edit peach-card-number"515 placeholder="•••• •••• •••• ••••"516 autocomplete="cc-number" inputmode="numeric" maxLength={19}517 value={formatCardNumberDisplay(cardNumber(), cardNumberFocused())}518 onFocus={() => setCardNumberFocused(true)}519 onBlur={() => setCardNumberFocused(false)}520 onInput={(e) => {521 const raw = (e.currentTarget as HTMLInputElement).value.replace(/\s/g, '');522 setCardNumber(raw);523 }} />524 </div>525 </div>526 {}527 <div class="peach-field-card">528 <span class="peach-label">expiry</span>529 <div class="peach-card-expiry-row">530 <input class="peach-field-value peach-field-value--edit" placeholder="MM"531 autocomplete="cc-exp-month" inputmode="numeric" maxLength={2} style="width:56px"532 value={expMonth()}533 onInput={(e) => setExpMonth((e.currentTarget as HTMLInputElement).value)} />534 <span class="peach-card-expiry-sep">/</span>535 <input class="peach-field-value peach-field-value--edit" placeholder="YYYY"536 autocomplete="cc-exp-year" inputmode="numeric" maxLength={4} style="width:72px"537 value={expYear()}538 onInput={(e) => setExpYear((e.currentTarget as HTMLInputElement).value)} />539 </div>540 </div>541 {}542 <div class="peach-field-card">543 <span class="peach-label">CVC</span>544 <div class="peach-field-value-row">545 <input class="peach-field-value peach-field-value--edit"546 type={showCvc() ? "text" : "password"} placeholder="•••"547 autocomplete="cc-csc" inputmode="numeric" maxLength={4}548 value={cvc()}549 onInput={(e) => setCvc((e.currentTarget as HTMLInputElement).value)} />550 <button class="peach-inline-action peach-password-toggle-btn" type="button"551 onClick={() => setShowCvc(!showCvc())} aria-label={showCvc() ? "Hide CVC" : "Show CVC"}>552 <Show when={showCvc()} fallback={553 <svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">554 <path d="M1 5C1 5 2.8 1 7 1C11.2 1 13 5 13 5C13 5 11.2 9 7 9C2.8 9 1 5 1 5Z" />555 <circle cx="7" cy="5" r="1.75" />556 </svg>557 }>558 <svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">559 <path d="M1 2C3 4.5 5 6 7 6C9 6 11 4.5 13 2" />560 </svg>561 </Show>562 </button>563 </div>564 </div>565 </section>566 </Show>567568 {}569 <Show when={selectedType() === "note"}>570 <div class="peach-note-body">571 <NoteEditorInline572 initialContent={notes()}573 onContentChange={setNotes}574 onReady={(el) => setNoteEditorEl(el)}575 />576 </div>577 </Show>578579 {}580 <Show when={selectedType() === "login" && (showNotes() || notes())}>581 <div class="peach-field-card" style="margin:0 12px">582 <span class="peach-label">notes</span>583 <textarea584 class="peach-new-textarea"585 placeholder="anything worth remembering"586 value={notes()}587 onInput={(e) => setNotes((e.currentTarget as HTMLTextAreaElement).value)}588 />589 </div>590 </Show>591592 {}593 <div class="peach-entry-add-links">594 <Show when={selectedType() === "login" && !showTotp() && !totp()}>595 <button class="peach-add-field-link" type="button" onClick={() => setShowTotp(true)}>596 + add 2FA597 </button>598 </Show>599 <Show when={selectedType() === "login" && !showNotes() && !notes()}>600 <button class="peach-add-field-link" type="button" onClick={() => setShowNotes(true)}>601 + add notes602 </button>603 </Show>604 </div>605606 {}607 <div class="peach-field-card" style="margin:4px 12px 12px">608 <span class="peach-label">tags</span>609 <div class="peach-tag-input-wrap">610 <For each={tags()}>611 {(tag) => (612 <span class="peach-tag-chip">613 {tag}614 <button class="peach-tag-chip-remove" onClick={() => removeTag(tag)} aria-label={`Remove ${tag}`}>×</button>615 </span>616 )}617 </For>618 <input619 class="peach-tag-input"620 placeholder={tags().length === 0 ? "add tags…" : ""}621 value={tagInput()}622 onInput={(e) => { setTagInput((e.currentTarget as HTMLInputElement).value); setShowTagSuggestions(true); }}623 onKeyDown={handleTagKeyDown}624 onFocus={() => setShowTagSuggestions(true)}625 onBlur={() => setTimeout(() => setShowTagSuggestions(false), 150)}626 style="flex:1;min-width:80px;background:transparent;border:none;outline:none;color:var(--t1);font-size:12px;padding:2px;"627 />628 </div>629 <Show when={showTagSuggestions() && tagSuggestions().length > 0}>630 <div class="peach-tag-suggestions" style="margin-top:4px;">631 <For each={tagSuggestions()}>632 {(s) => (633 <button634 class="peach-tag-suggestion"635 onMouseDown={() => { addTag(s); setTagInput(""); setShowTagSuggestions(false); }}636 >{s}</button>637 )}638 </For>639 </div>640 </Show>641 </div>642643 <Show when={error()}>644 <p class="peach-hint peach-hint-error" style="margin:0 16px 12px">{error()}</p>645 </Show>646647 </div>648 </div>649 );650}651652interface IdentitySuggestListProps {653 suggestions: Array<{ value: string; count: number; type: "email" | "username" }>;654 filter: "all" | "username" | "email";655 onFilter: (f: "all" | "username" | "email") => void;656 onPick: (value: string) => void;657}658659function IdentitySuggestList(props: IdentitySuggestListProps) {660 const [collapsed, setCollapsed] = createSignal(false);661 const ChevronIcon = () => (662 <svg width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round">663 <path d="M1 1L5 5L9 1" />664 </svg>665 );666 return (667 <div class="peach-identity-suggestions">668 <div class="peach-identity-filter-row">669 <button class={`peach-identity-filter-pill ${props.filter === "all" ? "is-on" : ""}`} type="button" onMouseDown={(e) => { e.preventDefault(); props.onFilter("all"); }}>all</button>670 <button class={`peach-identity-filter-pill ${props.filter === "username" ? "is-on" : ""}`} type="button" onMouseDown={(e) => { e.preventDefault(); props.onFilter("username"); }}>user</button>671 <button class={`peach-identity-filter-pill ${props.filter === "email" ? "is-on" : ""}`} type="button" onMouseDown={(e) => { e.preventDefault(); props.onFilter("email"); }}>email</button>672 <button673 class={`peach-identity-collapse-btn${collapsed() ? " is-collapsed" : ""}`}674 type="button"675 onMouseDown={(e) => { e.preventDefault(); setCollapsed(c => !c); }}676 aria-label={collapsed() ? "Show suggestions" : "Hide suggestions"}677 >678 <ChevronIcon />679 </button>680 </div>681 <div class={`peach-identity-rows${collapsed() ? " is-collapsed" : ""}`}>682 <div class="peach-identity-rows-inner">683 <For each={props.suggestions}>684 {(s) => (685 <button class="peach-identity-suggestion" type="button" onMouseDown={() => props.onPick(s.value)}>686 <span class="peach-identity-suggestion-value">{s.value}</span>687 <span class={`peach-identity-suggestion-type peach-identity-suggestion-type--${s.type}`}>{s.type === "email" ? "@" : "u"}</span>688 <span class="peach-identity-suggestion-meta">{s.count}×</span>689 </button>690 )}691 </For>692 </div>693 </div>694 </div>695 );696}