An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add HomeScreen component with identity card and status indicators

Implements Task 1 of Phase 3 (MM-150).

The HomeScreen component displays:
- Identity card with DID avatar, handle, truncated DID with copy button, and email
- Status indicators for relay health and session state
- Action buttons for viewing DID document, recovery info, and logging out

Verifies MM-150.AC1.1-AC1.8 (identity card display with truncation and avatar),
MM-150.AC2.1-AC2.5 (status indicators), and MM-150.AC3.1-AC3.4, AC3.8, AC3.10
(action flows including logout, navigation to DID document, and recovery info).

DID truncation shows 'did:plc:' prefix in full, then first 8 + '…' + last 6 chars
of the method-specific ID (when >= 14 chars).

authored by

Malpercio and committed by
Tangled
e2693fe1 f8da30f1

+342
+342
apps/identity-wallet/src/lib/components/home/HomeScreen.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { loadHomeData, logOut, type HomeData } from '$lib/ipc'; 4 + import DIDAvatar from './DIDAvatar.svelte'; 5 + 6 + let { 7 + onnavdiddoc, 8 + onnavrecovery, 9 + onlogout, 10 + }: { 11 + onnavdiddoc: (data: HomeData) => void; 12 + onnavrecovery: (data: HomeData) => void; 13 + onlogout: () => void; 14 + } = $props(); 15 + 16 + let homeData = $state<HomeData | null>(null); 17 + let loading = $state(true); 18 + let didCopied = $state(false); 19 + 20 + async function loadData() { 21 + loading = true; 22 + homeData = await loadHomeData(); 23 + loading = false; 24 + } 25 + 26 + onMount(() => { 27 + loadData(); 28 + }); 29 + 30 + // Truncate the DID for display on narrow mobile screens. 31 + // "did:plc:abcdefghijklmnopqrstuvwx" → "did:plc:abcdefgh…uvwxyz" 32 + let displayDid = $derived.by(() => { 33 + const did = homeData?.session?.did ?? ''; 34 + const prefix = 'did:plc:'; 35 + if (!did.startsWith(prefix)) return did; 36 + const specific = did.slice(prefix.length); 37 + if (specific.length < 14) return did; 38 + return `${prefix}${specific.slice(0, 8)}…${specific.slice(-6)}`; 39 + }); 40 + 41 + async function copyDid() { 42 + const did = homeData?.session?.did; 43 + if (!did) return; 44 + try { 45 + await navigator.clipboard.writeText(did); 46 + didCopied = true; 47 + setTimeout(() => { didCopied = false; }, 2000); 48 + } catch (e) { 49 + console.error('clipboard write failed:', e); 50 + } 51 + } 52 + 53 + async function handleLogOut() { 54 + await logOut(); 55 + onlogout(); 56 + } 57 + </script> 58 + 59 + {#if loading} 60 + <div class="screen screen--center"> 61 + <div class="spinner" aria-label="Loading"></div> 62 + <p class="status-text">Loading…</p> 63 + </div> 64 + {:else} 65 + <div class="screen"> 66 + <div class="header"> 67 + <h1 class="title">Identity Wallet</h1> 68 + <button class="refresh-btn" onclick={loadData} aria-label="Refresh">↻</button> 69 + </div> 70 + 71 + {#if homeData?.session} 72 + <!-- Identity card --> 73 + <div class="identity-card"> 74 + <DIDAvatar did={homeData.session.did} handle={homeData.session.handle} /> 75 + <div class="identity-info"> 76 + <p class="handle">@{homeData.session.handle}</p> 77 + <button class="did-btn" onclick={copyDid} title="Tap to copy full DID"> 78 + <span class="did-text">{displayDid}</span> 79 + <span class="copy-hint">{didCopied ? 'Copied!' : 'Copy'}</span> 80 + </button> 81 + <p class="email">{homeData.session.email}</p> 82 + </div> 83 + </div> 84 + {:else} 85 + <div class="identity-card identity-card--empty"> 86 + <p class="empty-text">Session unavailable</p> 87 + {#if homeData?.sessionError} 88 + <p class="error-code">{homeData.sessionError}</p> 89 + {/if} 90 + </div> 91 + {/if} 92 + 93 + <!-- Status indicators --> 94 + <div class="status-section"> 95 + <div class="status-row"> 96 + <span 97 + class="status-dot" 98 + class:status-dot--ok={homeData?.relayHealthy} 99 + class:status-dot--err={!homeData?.relayHealthy} 100 + aria-hidden="true" 101 + ></span> 102 + <div> 103 + <p class="status-label">Relay</p> 104 + <p class="status-value">{homeData?.relayHealthy ? 'Connected' : 'Error'}</p> 105 + </div> 106 + </div> 107 + <div class="status-row"> 108 + <span 109 + class="status-dot" 110 + class:status-dot--ok={homeData?.session != null} 111 + class:status-dot--err={homeData?.session == null} 112 + aria-hidden="true" 113 + ></span> 114 + <div> 115 + <p class="status-label">Session</p> 116 + <p class="status-value">{homeData?.session != null ? 'Active' : 'Error'}</p> 117 + </div> 118 + </div> 119 + </div> 120 + 121 + <!-- Action buttons --> 122 + <div class="actions"> 123 + {#if homeData?.session?.didDoc != null} 124 + <button class="action-btn" onclick={() => onnavdiddoc(homeData!)}> 125 + View DID Document 126 + </button> 127 + {/if} 128 + <button class="action-btn" onclick={() => onnavrecovery(homeData!)}> 129 + Recovery Info 130 + </button> 131 + <button class="action-btn action-btn--danger" onclick={handleLogOut}> 132 + Log Out 133 + </button> 134 + </div> 135 + </div> 136 + {/if} 137 + 138 + <style> 139 + .screen { 140 + display: flex; 141 + flex-direction: column; 142 + height: 100%; 143 + padding: 2rem 1.5rem; 144 + gap: 1.5rem; 145 + overflow-y: auto; 146 + } 147 + 148 + .screen--center { 149 + align-items: center; 150 + justify-content: center; 151 + gap: 1rem; 152 + } 153 + 154 + .spinner { 155 + width: 48px; 156 + height: 48px; 157 + border: 4px solid #e5e7eb; 158 + border-top-color: #007aff; 159 + border-radius: 50%; 160 + animation: spin 0.8s linear infinite; 161 + } 162 + 163 + @keyframes spin { 164 + to { transform: rotate(360deg); } 165 + } 166 + 167 + .status-text { 168 + font-size: 1rem; 169 + color: #6b7280; 170 + margin: 0; 171 + } 172 + 173 + .header { 174 + display: flex; 175 + align-items: center; 176 + justify-content: space-between; 177 + } 178 + 179 + .title { 180 + font-size: 1.4rem; 181 + font-weight: 700; 182 + margin: 0; 183 + color: #111827; 184 + } 185 + 186 + .refresh-btn { 187 + background: none; 188 + border: none; 189 + font-size: 1.4rem; 190 + color: #007aff; 191 + cursor: pointer; 192 + padding: 0.25rem; 193 + line-height: 1; 194 + } 195 + 196 + .identity-card { 197 + background: #f9fafb; 198 + border: 1px solid #d1d5db; 199 + border-radius: 12px; 200 + padding: 1.25rem; 201 + display: flex; 202 + align-items: center; 203 + gap: 1rem; 204 + } 205 + 206 + .identity-card--empty { 207 + flex-direction: column; 208 + align-items: flex-start; 209 + } 210 + 211 + .identity-info { 212 + display: flex; 213 + flex-direction: column; 214 + gap: 0.25rem; 215 + min-width: 0; 216 + } 217 + 218 + .handle { 219 + font-size: 1.1rem; 220 + font-weight: 600; 221 + color: #111827; 222 + margin: 0; 223 + white-space: nowrap; 224 + overflow: hidden; 225 + text-overflow: ellipsis; 226 + } 227 + 228 + .did-btn { 229 + background: none; 230 + border: none; 231 + padding: 0; 232 + cursor: pointer; 233 + display: flex; 234 + align-items: center; 235 + gap: 0.5rem; 236 + text-align: left; 237 + } 238 + 239 + .did-text { 240 + font-family: monospace; 241 + font-size: 0.8rem; 242 + color: #374151; 243 + } 244 + 245 + .copy-hint { 246 + font-size: 0.7rem; 247 + color: #007aff; 248 + white-space: nowrap; 249 + } 250 + 251 + .email { 252 + font-size: 0.85rem; 253 + color: #6b7280; 254 + margin: 0; 255 + white-space: nowrap; 256 + overflow: hidden; 257 + text-overflow: ellipsis; 258 + } 259 + 260 + .empty-text { 261 + font-size: 0.95rem; 262 + color: #6b7280; 263 + margin: 0; 264 + } 265 + 266 + .error-code { 267 + font-family: monospace; 268 + font-size: 0.8rem; 269 + color: #ef4444; 270 + margin: 0; 271 + } 272 + 273 + .status-section { 274 + background: #f9fafb; 275 + border: 1px solid #d1d5db; 276 + border-radius: 12px; 277 + padding: 1rem 1.25rem; 278 + display: flex; 279 + flex-direction: column; 280 + gap: 0.75rem; 281 + } 282 + 283 + .status-row { 284 + display: flex; 285 + align-items: center; 286 + gap: 0.75rem; 287 + } 288 + 289 + .status-dot { 290 + width: 10px; 291 + height: 10px; 292 + border-radius: 50%; 293 + flex-shrink: 0; 294 + } 295 + 296 + .status-dot--ok { 297 + background: #22c55e; 298 + } 299 + 300 + .status-dot--err { 301 + background: #ef4444; 302 + } 303 + 304 + .status-label { 305 + font-size: 0.75rem; 306 + font-weight: 600; 307 + color: #6b7280; 308 + margin: 0; 309 + text-transform: uppercase; 310 + letter-spacing: 0.04em; 311 + } 312 + 313 + .status-value { 314 + font-size: 0.875rem; 315 + color: #111827; 316 + margin: 0; 317 + } 318 + 319 + .actions { 320 + display: flex; 321 + flex-direction: column; 322 + gap: 0.75rem; 323 + margin-top: auto; 324 + } 325 + 326 + .action-btn { 327 + width: 100%; 328 + padding: 0.9rem; 329 + background: #007aff; 330 + color: #fff; 331 + border: none; 332 + border-radius: 12px; 333 + font-size: 1rem; 334 + font-weight: 600; 335 + cursor: pointer; 336 + } 337 + 338 + .action-btn--danger { 339 + background: #f3f4f6; 340 + color: #ef4444; 341 + } 342 + </style>