1import { createSignal, Show } from "solid-js";23import type { PasskeyEntry } from "../../shared/types";4import { detectBrowser } from "../../shared/browser";56interface PasskeyEntryProps {7 entry: PasskeyEntry | null;8 onDelete: () => void | Promise<void>;9}1011function formatDate(timestamp?: number): string {12 if (!timestamp) return "Never";13 return new Date(timestamp).toLocaleDateString();14}1516function formatUsedCount(count?: number): string {17 const used = count ?? 0;18 return `used ${used} time${used === 1 ? "" : "s"}`;19}2021function algorithmLabel(algorithm: -7 | -8): string {22 return algorithm === -8 ? "EdDSA (Ed25519)" : "ES256 (P-256)";23}2425function titleSizeClass(title: string): string {26 if (title.length >= 28) return "peach-entry-name--xxl";27 if (title.length >= 20) return "peach-entry-name--xl";28 if (title.length >= 14) return "peach-entry-name--lg";29 return "";30}3132export function PasskeyEntryView(props: PasskeyEntryProps) {33 const [deleteConfirmPending, setDeleteConfirmPending] = createSignal(false);34 const currentBrowser = detectBrowser();3536 function crossBrowserMessage(entryBrowser: "chrome" | "firefox" | undefined): string | null {37 if (!entryBrowser) return null;38 if (currentBrowser === "unknown") return null;39 if (entryBrowser === currentBrowser) return null;40 const from = entryBrowser === "chrome" ? "Chrome" : "Firefox";41 const to = currentBrowser === "chrome" ? "Chrome" : "Firefox";42 return `This passkey was created on ${from}. To use it on ${to}, re-register it on this device.`;43 }4445 return (46 <div class="peach-stage" style="position:relative;">47 <Show when={props.entry} fallback={<div class="peach-list-empty"><span>no passkey selected</span></div>}>48 {(entry) => (49 <>50 <div51 class="peach-ghost"52 aria-hidden="true"53 style="position:absolute;top:30px;left:12px;right:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:150px;font-style:italic;font-weight:400;color:var(--t0);opacity:0.03;pointer-events:none;line-height:1;overflow:hidden;white-space:nowrap;z-index:0;"54 >55 {entry().title}56 </div>5758 <div class="peach-entry-name-wrap" style="position:relative;z-index:1;">59 <h1 class={`peach-entry-name ${titleSizeClass(entry().title)}`} aria-label={entry().title}>60 <span class="peach-entry-name-text">{entry().title}</span>61 </h1>62 </div>6364 <div class="peach-domain" style="position:relative;z-index:1;">65 <div class="peach-domain-row">66 <span class="peach-domain-value">{entry().rpId}</span>67 <span class="peach-passkey-badge">passkey</span>68 </div>69 </div>7071 <section class="peach-fields" style="position:relative;z-index:1;">72 <div class="peach-field-card">73 <span class="peach-label">username</span>74 <div class="peach-field-value-row">75 <span class="peach-field-value">{entry().userName}</span>76 </div>77 </div>7879 <div class="peach-field-card">80 <span class="peach-label">display name</span>81 <div class="peach-field-value-row">82 <span class="peach-field-value">{entry().userDisplayName}</span>83 </div>84 </div>8586 <div class="peach-field-card">87 <span class="peach-label">algorithm</span>88 <div class="peach-field-value-row">89 <span class="peach-field-value">{algorithmLabel(entry().algorithm)}</span>90 </div>91 </div>9293 <div class="peach-field-card">94 <span class="peach-label">activity</span>95 <div class="peach-field-value-row peach-passkey-activity">96 <span class="peach-field-value">{formatUsedCount(entry().signCount)}</span>97 <span class="peach-passkey-meta">created {formatDate(entry().createdAt)}</span>98 <span class="peach-passkey-meta">last used {formatDate(entry().lastUsedAt)}</span>99 </div>100 </div>101 </section>102103 <div class="peach-passkey-actions">104 <Show when={crossBrowserMessage(entry().browser)}>105 {(msg) => (106 <div class="peach-info-notice peach-info-notice--warn">107 {msg()}108 </div>109 )}110 </Show>111 <div class="peach-info-notice">112 {entry().browser === "firefox"113 ? "This passkey is held by your platform authenticator. Peach stores only the metadata."114 : "The private key is stored encrypted in your Peach vault and is never shown in the UI."}115 </div>116 <Show117 when={deleteConfirmPending()}118 fallback={119 <button120 class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn"121 onClick={() => setDeleteConfirmPending(true)}122 >123 remove passkey124 </button>125 }126 >127 <div class="peach-passkey-delete-confirm">128 <span class="peach-passkey-delete-confirm-copy">Remove this passkey?</span>129 <button130 class="peach-btn peach-btn--ghost peach-btn--sm"131 onClick={() => setDeleteConfirmPending(false)}132 >133 cancel134 </button>135 <button136 class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn--confirm"137 onClick={() => {138 setDeleteConfirmPending(false);139 void props.onDelete();140 }}141 >142 remove143 </button>144 </div>145 </Show>146 </div>147 </>148 )}149 </Show>150 </div>151 );152}