PasskeyEntry.tsx

5.1 KB · raw

1import { createSignal, Show } from "solid-js";23import type { PasskeyEntry } from "../../shared/types";45interface PasskeyEntryProps {6  entry: PasskeyEntry | null;7  onDelete: () => void | Promise<void>;8}910function formatDate(timestamp?: number): string {11  if (!timestamp) return "Never";12  return new Date(timestamp).toLocaleDateString();13}1415function formatUsedCount(count?: number): string {16  const used = count ?? 0;17  return `used ${used} time${used === 1 ? "" : "s"}`;18}1920function algorithmLabel(algorithm: -7 | -8): string {21  return algorithm === -8 ? "EdDSA (Ed25519)" : "ES256 (P-256)";22}2324function titleSizeClass(title: string): string {25  if (title.length >= 28) return "peach-entry-name--xxl";26  if (title.length >= 20) return "peach-entry-name--xl";27  if (title.length >= 14) return "peach-entry-name--lg";28  return "";29}3031export function PasskeyEntryView(props: PasskeyEntryProps) {32  const [deleteConfirmPending, setDeleteConfirmPending] = createSignal(false);3334  return (35    <div class="peach-stage" style="position:relative;">36      <Show when={props.entry} fallback={<div class="peach-list-empty"><span>no passkey selected</span></div>}>37        {(entry) => (38          <>39            <div40              class="peach-ghost"41              aria-hidden="true"42              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;"43            >44              {entry().title}45            </div>4647            <div class="peach-entry-name-wrap" style="position:relative;z-index:1;">48              <h1 class={`peach-entry-name ${titleSizeClass(entry().title)}`} aria-label={entry().title}>49                <span class="peach-entry-name-text">{entry().title}</span>50              </h1>51            </div>5253            <div class="peach-domain" style="position:relative;z-index:1;">54              <div class="peach-domain-row">55                <span class="peach-domain-value">{entry().rpId}</span>56                <span class="peach-passkey-badge">passkey</span>57              </div>58            </div>5960            <section class="peach-fields" style="position:relative;z-index:1;">61              <div class="peach-field-card">62                <span class="peach-label">username</span>63                <div class="peach-field-value-row">64                  <span class="peach-field-value">{entry().userName}</span>65                </div>66              </div>6768              <div class="peach-field-card">69                <span class="peach-label">display name</span>70                <div class="peach-field-value-row">71                  <span class="peach-field-value">{entry().userDisplayName}</span>72                </div>73              </div>7475              <div class="peach-field-card">76                <span class="peach-label">algorithm</span>77                <div class="peach-field-value-row">78                  <span class="peach-field-value">{algorithmLabel(entry().algorithm)}</span>79                </div>80              </div>8182              <div class="peach-field-card">83                <span class="peach-label">activity</span>84                <div class="peach-field-value-row peach-passkey-activity">85                  <span class="peach-field-value">{formatUsedCount(entry().signCount)}</span>86                  <span class="peach-passkey-meta">created {formatDate(entry().createdAt)}</span>87                  <span class="peach-passkey-meta">last used {formatDate(entry().lastUsedAt)}</span>88                </div>89              </div>90            </section>9192            <div class="peach-passkey-actions">93              <div class="peach-info-notice">94                The private key is stored encrypted in your Peach vault and is never shown in the UI.95              </div>96              <Show97                when={deleteConfirmPending()}98                fallback={99                  <button100                    class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn"101                    onClick={() => setDeleteConfirmPending(true)}102                  >103                    remove passkey104                  </button>105                }106              >107                <div class="peach-passkey-delete-confirm">108                  <span class="peach-passkey-delete-confirm-copy">Remove this passkey?</span>109                  <button110                    class="peach-btn peach-btn--ghost peach-btn--sm"111                    onClick={() => setDeleteConfirmPending(false)}112                  >113                    cancel114                  </button>115                  <button116                    class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn--confirm"117                    onClick={() => {118                      setDeleteConfirmPending(false);119                      void props.onDelete();120                    }}121                  >122                    remove123                  </button>124                </div>125              </Show>126            </div>127          </>128        )}129      </Show>130    </div>131  );132}