1import { createSignal, For, 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-header" style="position:relative;z-index:1;">59 <div class="peach-entry-header-name">60 <h1 class={`peach-entry-name ${titleSizeClass(entry().title)}`} aria-label={entry().title}>61 <span class="peach-entry-name-text">{entry().title}</span>62 </h1>63 </div>64 <Show65 when={deleteConfirmPending()}66 fallback={67 <button68 class="peach-entry-trash-btn"69 onClick={() => setDeleteConfirmPending(true)}70 aria-label="Remove passkey"71 title="Remove passkey"72 >73 <svg width="12" height="13" viewBox="0 0 12 13" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">74 <path d="M1 3h10M4 3V2h4v1M2 3l.7 8.5a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9L10 3" />75 <path d="M4.5 6l.3 3.5M7.5 6l-.3 3.5" />76 </svg>77 </button>78 }79 >80 <div class="peach-entry-trash-confirm">81 <span class="peach-entry-trash-confirm-label">remove?</span>82 <button83 class="peach-btn peach-btn--sm peach-btn--danger"84 style="font-size:10px;padding:2px 8px;min-height:22px;"85 onClick={() => {86 setDeleteConfirmPending(false);87 void props.onDelete();88 }}89 >90 yes91 </button>92 <button93 class="peach-btn peach-btn--ghost peach-btn--sm"94 style="font-size:10px;padding:2px 8px;min-height:22px;"95 onClick={() => setDeleteConfirmPending(false)}96 >97 no98 </button>99 </div>100 </Show>101 </div>102103 <div class="peach-domain" style="position:relative;z-index:1;">104 <div class="peach-domain-row">105 <span class="peach-domain-value">{entry().rpId}</span>106 <span class="peach-passkey-badge">passkey</span>107 </div>108 </div>109110 <section class="peach-fields" style="position:relative;z-index:1;">111 <div class="peach-field-card">112 <span class="peach-label">username</span>113 <div class="peach-field-value-row">114 <span class="peach-field-value">{entry().userName}</span>115 </div>116 </div>117118 <div class="peach-field-card">119 <span class="peach-label">display name</span>120 <div class="peach-field-value-row">121 <span class="peach-field-value">{entry().userDisplayName}</span>122 </div>123 </div>124125 <div class="peach-field-card">126 <span class="peach-label">algorithm</span>127 <div class="peach-field-value-row">128 <span class="peach-field-value">{algorithmLabel(entry().algorithm)}</span>129 </div>130 </div>131132 <div class="peach-field-card">133 <span class="peach-label">activity</span>134 <div class="peach-field-value-row peach-passkey-activity">135 <span class="peach-field-value">{formatUsedCount(entry().signCount)}</span>136 <span class="peach-passkey-meta">created {formatDate(entry().createdAt)}</span>137 <span class="peach-passkey-meta">last used {formatDate(entry().lastUsedAt)}</span>138 </div>139 </div>140 </section>141142 <Show when={entry().tags && entry().tags!.length > 0}>143 <div144 style="display:flex;flex-wrap:wrap;gap:5px;margin:6px 0 2px;position:relative;z-index:1;"145 >146 <For each={entry().tags!}>147 {(tag) => <span class="peach-tag-chip" style="font-size:10px;padding:2px 8px;">{tag}</span>}148 </For>149 </div>150 </Show>151152 <div class="peach-passkey-actions">153 <Show when={crossBrowserMessage(entry().browser)}>154 {(msg) => (155 <div class="peach-info-notice peach-info-notice--warn">156 {msg()}157 </div>158 )}159 </Show>160 <div class="peach-info-notice">161 {entry().browser === "firefox"162 ? "This passkey is held by your platform authenticator. Peach stores only the metadata."163 : "The private key is stored encrypted in your Peach vault and is never shown in the UI."}164 </div>165 </div>166 </>167 )}168 </Show>169 </div>170 );171}