1import { createSignal, createMemo, createEffect, For, Show, onMount } 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 [identityFilter, setIdentityFilter] = createSignal<"all" | "username" | "email">("all");3738 const allIdentities = createMemo(() => {39 const emailMap = new Map<string, number>();40 const usernameMap = new Map<string, number>();41 for (const entry of props.entries ?? []) {42 const uname = entry.username?.trim() ?? "";43 const em = (entry as VaultEntry & { email?: string }).email?.trim() ?? "";44 if (uname) usernameMap.set(uname, (usernameMap.get(uname) ?? 0) + 1);45 if (em) emailMap.set(em, (emailMap.get(em) ?? 0) + 1);46 }47 const result: Array<{ value: string; count: number; type: "email" | "username" }> = [];48 const seen = new Set<string>();49 for (const v of new Set([...emailMap.keys(), ...usernameMap.keys()])) {50 if (seen.has(v)) continue;51 seen.add(v);52 const ec = emailMap.get(v) ?? 0;53 const uc = usernameMap.get(v) ?? 0;54 result.push({ value: v, count: Math.max(ec, uc), type: ec >= uc ? "email" : "username" });55 }56 return result.sort((a, b) => b.count - a.count);57 });5859 const identitySuggestions = createMemo(() => {60 const field = activeIdentityField();61 if (!field) return [];62 const input = (field === "username" ? username() : email()).toLowerCase();63 const filter = identityFilter();64 return allIdentities()65 .filter(s => (filter === "all" || s.type === filter) && (!input || s.value.toLowerCase().includes(input)))66 .slice(0, 8);67 });6869 const isNoteEntry = () => props.entry?.type === "note";70 const isCardEntry = () => props.entry?.type === "card";71 const [selectedType, setSelectedType] = createSignal<"login" | "note" | "card">(72 isNoteEntry() ? "note" : isCardEntry() ? "card" : "login"73 );7475 76 const [cardNickname, setCardNickname] = createSignal(props.entry?.cardNickname ?? "");77 const [cardholderName, setCardholderName] = createSignal(props.entry?.cardholderName ?? "");78 const [cardNumber, setCardNumber] = createSignal(props.entry?.cardNumber ?? "");79 const [expMonth, setExpMonth] = createSignal(props.entry?.expMonth ?? "");80 const [expYear, setExpYear] = createSignal(props.entry?.expYear ?? "");81 const [cvc, setCvc] = createSignal(props.entry?.cvc ?? "");82 const [showCvc, setShowCvc] = createSignal(false);83 const [cardNumberFocused, setCardNumberFocused] = createSignal(false);84 const [noteEditorEl, setNoteEditorEl] = createSignal<HTMLDivElement | null>(null);8586 function noteExec(cmd: string, val?: string) {87 document.execCommand(cmd, false, val);88 noteEditorEl()?.focus();89 }9091 function getCardNetwork(num: string): string {92 const n = num.replace(/\s/g, '');93 if (/^4/.test(n)) return 'Visa';94 if (/^5[1-5]|^2[2-7]/.test(n)) return 'Mastercard';95 if (/^3[47]/.test(n)) return 'Amex';96 if (/^6(?:011|5)/.test(n)) return 'Discover';97 return '';98 }99100 function formatCardNumberDisplay(raw: string, focused: boolean): string {101 const n = raw.replace(/\s/g, '');102 if (!focused && n.length >= 4) return '•••• •••• •••• ' + n.slice(-4);103 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(' '));104 return n.replace(/(\d{4})(?=\d)/g, '$1 ').trim();105 }106107 const canSave = () => {108 if (!title().trim()) return false;109 if (selectedType() === "login" && !password().trim()) return false;110 if (selectedType() === "card" && (!cardholderName().trim() || !cardNumber().trim())) return false;111 return true;112 };113114 const allTags = createMemo(() => {115 const tagSet = new Set<string>();116 for (const entry of props.entries ?? []) {117 for (const tag of entry.tags ?? []) tagSet.add(tag);118 }119 return Array.from(tagSet).sort();120 });121122 const tagSuggestions = createMemo(() => {123 const input = tagInput().toLowerCase().trim();124 if (!input) return [];125 return allTags().filter(t =>126 t.toLowerCase().includes(input) && !tags().includes(t)127 ).slice(0, 5);128 });129130 function addTag(tag: string) {131 const n = tag.toLowerCase().trim();132 if (!n || n.length > 32 || tags().includes(n) || tags().length >= 10) return;133 setTags([...tags(), n]);134 }135136 function removeTag(tag: string) {137 setTags(tags().filter(t => t !== tag));138 }139140 function handleTagKeyDown(e: KeyboardEvent) {141 if (e.key === "Enter" || e.key === ",") {142 e.preventDefault();143 if (tagInput()) { addTag(tagInput()); setTagInput(""); setShowTagSuggestions(false); }144 } else if (e.key === "Backspace" && !tagInput() && tags().length > 0) {145 removeTag(tags()[tags().length - 1]);146 }147 }148149 150 createEffect(async () => {151 const pwd = password();152 if (pwd.length < 4) { setPasswordStrength(null); return; }153 try {154 if (!zxcvbnFn()) {155 const mod = await import("zxcvbn");156 setZxcvbnFn(() => mod.default);157 }158 const fn = zxcvbnFn();159 if (fn) setPasswordStrength(fn(pwd).score);160 } catch {161 setPasswordStrength(pwd.length >= 16 ? 3 : pwd.length >= 12 ? 2 : 1);162 }163 });164165 onMount(() => {166 const el = document.querySelector(".peach-new-title-input") as HTMLInputElement | null;167 if (el) el.focus();168 });169170 function getNormalizedTotp(): string | undefined {171 const value = totp().trim();172 if (!value) return undefined;173 if (value.startsWith("otpauth://")) {174 const parsed = parseOtpauthURI(value);175 return parsed?.secret ?? value;176 }177 return value;178 }179180 async function handleSave() {181 if (!canSave()) {182 setError(183 selectedType() === "note"184 ? "title is required"185 : selectedType() === "card"186 ? "cardholder name and card number are required"187 : "title and password are required"188 );189 return;190 }191192 setSaving(true);193 setError("");194195 const payload: Record<string, unknown> = {196 type: selectedType(),197 title: title().trim(),198 notes: notes().trim() || undefined,199 tags: tags().length > 0 ? tags() : undefined,200 };201202 if (selectedType() === "login") {203 payload.username = username().trim();204 payload.email = email().trim() || undefined;205 payload.password = password();206 payload.url = url().trim();207 payload.totp = getNormalizedTotp();208 }209210 if (selectedType() === "card") {211 const raw = cardNumber().replace(/\s/g, '');212 payload.cardholderName = cardholderName().trim();213 payload.cardNumber = raw;214 payload.expMonth = expMonth().trim();215 payload.expYear = expYear().trim();216 payload.cvc = cvc().trim();217 payload.cardNickname = cardNickname().trim() || undefined;218 const n = raw;219 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';220 payload.last4 = raw.slice(-4);221 }222223 if (props.entry) {224 const res = await sendMessage({ type: "UPDATE_ENTRY", payload: { id: props.entry.id, patch: payload } });225 const result = res as { ok: boolean; error?: string };226 if (!result.ok) { setError(result.error ?? "could not update entry"); setSaving(false); return; }227 await props.onSave({ ...props.entry, ...payload } as VaultEntry);228 return;229 }230231 const res = await sendMessage({ type: "ADD_ENTRY", payload });232 const result = res as { ok: boolean; data?: VaultEntry; error?: string };233 if (!result.ok) {234 if (result.error === "vault locked") {235 setError("vault is locked — tap the lock icon to re-enter your master password");236 } else {237 setError(result.error ?? "could not save entry");238 }239 setSaving(false);240 return;241 }242 await props.onSave(result.data);243 }244245 const strengthLabel = () => {246 const s = passwordStrength();247 if (s === null) return null;248 return ["weak", "weak", "fair", "good", "strong"][s] as string;249 };250251 return (252 <div class="peach-overlay-view ov-enter-right" style="display:flex">253 <div class="peach-overlay-header">254 <button255 class="peach-btn peach-btn--ghost peach-btn--sm peach-overlay-action"256 onClick={props.onBack}257 >258 cancel259 </button>260 <span class="peach-overlay-title">261 {props.entry ? "edit entry" : "new entry"}262 </span>263 <button264 class="peach-btn peach-btn--accent peach-btn--sm peach-overlay-action"265 onClick={() => void handleSave()}266 disabled={saving() || !canSave()}267 >268 {saving() ? "saving…" : props.entry ? "save" : "create"}269 </button>270 </div>271272 {}273 <div class="peach-entry-body peach-entry-body--new">274275 {}276 <Show when={!props.entry}>277 <div class="peach-new-type-row">278 <button279 class={`peach-new-type-btn ${selectedType() === "login" ? "is-active" : ""}`}280 onClick={() => setSelectedType("login")}281 >login</button>282 <button283 class={`peach-new-type-btn ${selectedType() === "note" ? "is-active" : ""}`}284 onClick={() => setSelectedType("note")}285 >note</button>286 <button287 class={`peach-new-type-btn ${selectedType() === "card" ? "is-active" : ""}`}288 onClick={() => setSelectedType("card")}289 >card</button>290 </div>291 </Show>292293 {}294 <Show when={selectedType() === "note"}>295 <Toolbar exec={noteExec} class="peach-note-toolbar-body" />296 </Show>297298 {}299 <div class="peach-new-title-wrap">300 <input301 class="peach-new-title-input"302 placeholder={selectedType() === "note" ? "note title" : "entry name"}303 autocomplete="off"304 value={title()}305 onInput={(e) => setTitle((e.currentTarget as HTMLInputElement).value)}306 onKeyDown={(e) => { if (e.key === "Enter") void handleSave(); }}307 />308 </div>309310 {}311 <Show when={selectedType() === "login"}>312 <div class="peach-domain">313 <input314 class="peach-domain-value peach-domain-value--edit"315 placeholder="domain.com"316 autocomplete="off"317 value={url()}318 onInput={(e) => setUrl((e.currentTarget as HTMLInputElement).value)}319 />320 </div>321322 <section class="peach-fields">323 {}324 <div class="peach-field-card">325 <span class="peach-label">username</span>326 <div class="peach-field-value-row">327 <input328 class="peach-field-value peach-field-value--edit"329 placeholder="johndoe"330 autocomplete="off"331 value={username()}332 onInput={(e) => setUsername((e.currentTarget as HTMLInputElement).value)}333 onFocus={() => setActiveIdentityField("username")}334 onBlur={() => setTimeout(() => setActiveIdentityField(null), 150)}335 />336 </div>337 <Show when={activeIdentityField() === "username" && identitySuggestions().length > 0}>338 <IdentitySuggestList339 suggestions={identitySuggestions()}340 filter={identityFilter()}341 onFilter={setIdentityFilter}342 onPick={(v) => { setUsername(v); setActiveIdentityField(null); }}343 />344 </Show>345 </div>346347 {}348 <div class="peach-field-card">349 <span class="peach-label">email</span>350 <div class="peach-field-value-row">351 <input352 class="peach-field-value peach-field-value--edit"353 type="email"354 placeholder="[email protected]"355 autocomplete="off"356 value={email()}357 onInput={(e) => setEmail((e.currentTarget as HTMLInputElement).value)}358 onFocus={() => setActiveIdentityField("email")}359 onBlur={() => setTimeout(() => setActiveIdentityField(null), 150)}360 />361 </div>362 <Show when={activeIdentityField() === "email" && identitySuggestions().length > 0}>363 <IdentitySuggestList364 suggestions={identitySuggestions()}365 filter={identityFilter()}366 onFilter={setIdentityFilter}367 onPick={(v) => { setEmail(v); setActiveIdentityField(null); }}368 />369 </Show>370 </div>371372 {}373 <div class="peach-field-card">374 <span class="peach-label">password</span>375 <div class="peach-password-edit-row">376 <input377 class="peach-field-value peach-field-value--edit"378 type={showPassword() ? "text" : "password"}379 placeholder="password"380 autocomplete="new-password"381 value={password()}382 onInput={(e) => setPassword((e.currentTarget as HTMLInputElement).value)}383 />384 <button385 class="peach-inline-action peach-password-toggle-btn"386 type="button"387 onClick={() => setShowPassword(!showPassword())}388 aria-label={showPassword() ? "Hide password" : "Show password"}389 >390 <Show391 when={showPassword()}392 fallback={393 <svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">394 <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" />395 <circle cx="7" cy="5" r="1.75" />396 </svg>397 }398 >399 <svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">400 <path d="M1 2C3 4.5 5 6 7 6C9 6 11 4.5 13 2" />401 <path d="M3.5 5.5L2.5 7.5" />402 <path d="M7 6.5V8.5" />403 <path d="M10.5 5.5L11.5 7.5" />404 </svg>405 </Show>406 </button>407 </div>408409 {}410 <Show when={strengthLabel() !== null}>411 <div class="peach-strength-hairline">412 <div class="peach-strength-hairline-track">413 <div414 class="peach-strength-hairline-fill"415 style={{416 width: ["25%","40%","60%","80%","100%"][passwordStrength()!],417 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()!],418 }}419 />420 </div>421 <span class="peach-strength-label">{strengthLabel()}</span>422 </div>423 </Show>424425 {}426 <button427 class="peach-generate-toggle-link"428 type="button"429 onClick={() => { setShowGenerator(!showGenerator()); }}430 >431 {showGenerator() ? "☰ hide generator" : "⚙ generate"}432 </button>433 <GeneratorDrawer434 compact435 open={showGenerator()}436 onUse={(pw) => { setPassword(pw); setShowGenerator(false); }}437 />438 </div>439440 {}441 <Show when={showTotp() || totp()}>442 <div class="peach-field-card">443 <span class="peach-label">2fa secret</span>444 <div class="peach-field-value-row">445 <input446 class="peach-field-value peach-field-value--edit"447 placeholder="BASE32 or otpauth:// URI"448 autocomplete="off"449 value={totp()}450 onInput={(e) => setTotp((e.currentTarget as HTMLInputElement).value)}451 />452 </div>453 </div>454 </Show>455 </section>456 </Show>457458 {}459 <Show when={selectedType() === "card"}>460 <section class="peach-fields">461 {}462 <div class="peach-field-card">463 <span class="peach-label">nickname</span>464 <div class="peach-field-value-row">465 <input class="peach-field-value peach-field-value--edit" placeholder="Personal Visa"466 autocomplete="off" value={cardNickname()}467 onInput={(e) => setCardNickname((e.currentTarget as HTMLInputElement).value)} />468 </div>469 </div>470 {}471 <div class="peach-field-card">472 <span class="peach-label">cardholder name</span>473 <div class="peach-field-value-row">474 <input class="peach-field-value peach-field-value--edit" placeholder="Jane Smith"475 autocomplete="cc-name" value={cardholderName()}476 onInput={(e) => setCardholderName((e.currentTarget as HTMLInputElement).value)} />477 </div>478 </div>479 {}480 <div class="peach-field-card">481 <span class="peach-label">482 card number483 <Show when={getCardNetwork(cardNumber())}>484 <span class="peach-card-network-badge">{getCardNetwork(cardNumber())}</span>485 </Show>486 </span>487 <div class="peach-field-value-row">488 <input class="peach-field-value peach-field-value--edit peach-card-number"489 placeholder="•••• •••• •••• ••••"490 autocomplete="cc-number" inputmode="numeric" maxLength={19}491 value={formatCardNumberDisplay(cardNumber(), cardNumberFocused())}492 onFocus={() => setCardNumberFocused(true)}493 onBlur={() => setCardNumberFocused(false)}494 onInput={(e) => {495 const raw = (e.currentTarget as HTMLInputElement).value.replace(/\s/g, '');496 setCardNumber(raw);497 }} />498 </div>499 </div>500 {}501 <div class="peach-field-card">502 <span class="peach-label">expiry</span>503 <div class="peach-card-expiry-row">504 <input class="peach-field-value peach-field-value--edit" placeholder="MM"505 autocomplete="cc-exp-month" inputmode="numeric" maxLength={2} style="width:56px"506 value={expMonth()}507 onInput={(e) => setExpMonth((e.currentTarget as HTMLInputElement).value)} />508 <span class="peach-card-expiry-sep">/</span>509 <input class="peach-field-value peach-field-value--edit" placeholder="YYYY"510 autocomplete="cc-exp-year" inputmode="numeric" maxLength={4} style="width:72px"511 value={expYear()}512 onInput={(e) => setExpYear((e.currentTarget as HTMLInputElement).value)} />513 </div>514 </div>515 {}516 <div class="peach-field-card">517 <span class="peach-label">CVC</span>518 <div class="peach-field-value-row">519 <input class="peach-field-value peach-field-value--edit"520 type={showCvc() ? "text" : "password"} placeholder="•••"521 autocomplete="cc-csc" inputmode="numeric" maxLength={4}522 value={cvc()}523 onInput={(e) => setCvc((e.currentTarget as HTMLInputElement).value)} />524 <button class="peach-inline-action peach-password-toggle-btn" type="button"525 onClick={() => setShowCvc(!showCvc())} aria-label={showCvc() ? "Hide CVC" : "Show CVC"}>526 <Show when={showCvc()} fallback={527 <svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">528 <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" />529 <circle cx="7" cy="5" r="1.75" />530 </svg>531 }>532 <svg width="14" height="10" viewBox="0 0 14 10" fill="none" stroke="currentColor" stroke-width="0.9" stroke-linecap="round">533 <path d="M1 2C3 4.5 5 6 7 6C9 6 11 4.5 13 2" />534 </svg>535 </Show>536 </button>537 </div>538 </div>539 </section>540 </Show>541542 {}543 <Show when={selectedType() === "note"}>544 <div class="peach-note-body">545 <NoteEditorInline546 initialContent={notes()}547 onContentChange={setNotes}548 onReady={(el) => setNoteEditorEl(el)}549 />550 </div>551 </Show>552553 {}554 <Show when={selectedType() === "login" && (showNotes() || notes())}>555 <div class="peach-field-card" style="margin:0 12px">556 <span class="peach-label">notes</span>557 <textarea558 class="peach-new-textarea"559 placeholder="anything worth remembering"560 value={notes()}561 onInput={(e) => setNotes((e.currentTarget as HTMLTextAreaElement).value)}562 />563 </div>564 </Show>565566 {}567 <div class="peach-entry-add-links">568 <Show when={selectedType() === "login" && !showTotp() && !totp()}>569 <button class="peach-add-field-link" type="button" onClick={() => setShowTotp(true)}>570 + add 2FA571 </button>572 </Show>573 <Show when={selectedType() === "login" && !showNotes() && !notes()}>574 <button class="peach-add-field-link" type="button" onClick={() => setShowNotes(true)}>575 + add notes576 </button>577 </Show>578 </div>579580 {}581 <div class="peach-field-card" style="margin:4px 12px 12px">582 <span class="peach-label">tags</span>583 <div class="peach-tag-input-wrap">584 <For each={tags()}>585 {(tag) => (586 <span class="peach-tag-chip">587 {tag}588 <button class="peach-tag-chip-remove" onClick={() => removeTag(tag)} aria-label={`Remove ${tag}`}>×</button>589 </span>590 )}591 </For>592 <input593 class="peach-tag-input"594 placeholder={tags().length === 0 ? "add tags…" : ""}595 value={tagInput()}596 onInput={(e) => { setTagInput((e.currentTarget as HTMLInputElement).value); setShowTagSuggestions(true); }}597 onKeyDown={handleTagKeyDown}598 onFocus={() => setShowTagSuggestions(true)}599 onBlur={() => setTimeout(() => setShowTagSuggestions(false), 150)}600 style="flex:1;min-width:80px;background:transparent;border:none;outline:none;color:var(--t1);font-size:12px;padding:2px;"601 />602 </div>603 <Show when={showTagSuggestions() && tagSuggestions().length > 0}>604 <div class="peach-tag-suggestions" style="margin-top:4px;">605 <For each={tagSuggestions()}>606 {(s) => (607 <button608 class="peach-tag-suggestion"609 onMouseDown={() => { addTag(s); setTagInput(""); setShowTagSuggestions(false); }}610 >{s}</button>611 )}612 </For>613 </div>614 </Show>615 </div>616617 <Show when={error()}>618 <p class="peach-hint peach-hint-error" style="margin:0 16px 12px">{error()}</p>619 </Show>620621 </div>622 </div>623 );624}625626interface IdentitySuggestListProps {627 suggestions: Array<{ value: string; count: number; type: "email" | "username" }>;628 filter: "all" | "username" | "email";629 onFilter: (f: "all" | "username" | "email") => void;630 onPick: (value: string) => void;631}632633function IdentitySuggestList(props: IdentitySuggestListProps) {634 return (635 <div class="peach-identity-suggestions">636 <div class="peach-identity-filter-row">637 <button class={`peach-identity-filter-pill ${props.filter === "all" ? "is-on" : ""}`} type="button" onMouseDown={() => props.onFilter("all")}>all</button>638 <button class={`peach-identity-filter-pill ${props.filter === "username" ? "is-on" : ""}`} type="button" onMouseDown={() => props.onFilter("username")}>user</button>639 <button class={`peach-identity-filter-pill ${props.filter === "email" ? "is-on" : ""}`} type="button" onMouseDown={() => props.onFilter("email")}>email</button>640 </div>641 <For each={props.suggestions}>642 {(s) => (643 <button class="peach-identity-suggestion" type="button" onMouseDown={() => props.onPick(s.value)}>644 <span class="peach-identity-suggestion-value">{s.value}</span>645 <span class={`peach-identity-suggestion-type peach-identity-suggestion-type--${s.type}`}>{s.type === "email" ? "@" : "u"}</span>646 <span class="peach-identity-suggestion-meta">{s.count}×</span>647 </button>648 )}649 </For>650 </div>651 );652}