Tools for the Atmosphere tools.slices.network
quickslice atproto html

feat(bugs): add Bluesky profile avatars and update How it works modal

- Add avatars next to handles in bug cards, detail view, responses, comments, and header
- Fetch avatars via GraphQL joins (appBskyActorProfileByDid) - no extra API calls
- Show first letter of handle as fallback when no avatar exists
- Add ring styling for logged-in user avatar in header
- Add Comments section to How it works modal
- Add Lexicons section with descriptions of each schema

+118 -7
+118 -7
bugs.html
··· 623 623 object-fit: cover; 624 624 } 625 625 626 + /* User avatars */ 627 + .user-avatar { 628 + width: 20px; 629 + height: 20px; 630 + border-radius: 50%; 631 + background: var(--border); 632 + flex-shrink: 0; 633 + overflow: hidden; 634 + display: inline-flex; 635 + align-items: center; 636 + justify-content: center; 637 + } 638 + 639 + .user-avatar img { 640 + width: 100%; 641 + height: 100%; 642 + object-fit: cover; 643 + } 644 + 645 + .user-avatar:not(:has(img)) { 646 + background: var(--accent); 647 + color: white; 648 + font-weight: 600; 649 + font-size: 0.625em; 650 + } 651 + 652 + .user-avatar-sm { 653 + width: 16px; 654 + height: 16px; 655 + } 656 + 657 + .user-avatar-lg { 658 + width: 24px; 659 + height: 24px; 660 + } 661 + 662 + .user-avatar-xl { 663 + width: 32px; 664 + height: 32px; 665 + } 666 + 667 + .user-avatar-ring { 668 + outline: 2px solid var(--border); 669 + outline-offset: 1px; 670 + } 671 + 672 + .user-info { 673 + display: inline-flex; 674 + align-items: center; 675 + gap: 0.375rem; 676 + } 677 + 626 678 .autocomplete-handle { 627 679 overflow: hidden; 628 680 text-overflow: ellipsis; ··· 753 805 } 754 806 755 807 .bug-card-meta { 808 + display: flex; 809 + align-items: center; 810 + gap: 0.25rem; 756 811 color: var(--text-secondary); 757 812 font-size: 0.875rem; 758 813 } ··· 844 899 } 845 900 846 901 .comment-meta { 902 + display: flex; 903 + align-items: center; 904 + gap: 0.25rem; 847 905 font-size: 0.75rem; 848 906 color: var(--text-secondary); 849 907 } ··· 1133 1191 return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 1134 1192 } 1135 1193 1194 + function renderAvatar(profile, handle, sizeClass = "") { 1195 + const avatarUrl = profile?.avatar?.url; 1196 + const className = `user-avatar${sizeClass ? ` ${sizeClass}` : ""}`; 1197 + if (avatarUrl) { 1198 + return `<span class="${className}"><img src="${esc(avatarUrl)}" alt=""></span>`; 1199 + } 1200 + const initial = handle ? handle.charAt(0).toUpperCase() : "?"; 1201 + return `<span class="${className}">${esc(initial)}</span>`; 1202 + } 1203 + 1136 1204 function showError(msg) { 1137 1205 const el = document.getElementById("error-banner"); 1138 1206 el.innerHTML = `<span>${esc(msg)}</span><button onclick="hideError()">×</button>`; ··· 1330 1398 namespace 1331 1399 createdAt 1332 1400 actorHandle 1401 + appBskyActorProfileByDid { 1402 + avatar { 1403 + url(preset: "avatar") 1404 + } 1405 + } 1333 1406 attachments { 1334 1407 ... on NetworkSlicesToolsDefsImages { 1335 1408 images { ··· 1398 1471 status 1399 1472 message 1400 1473 actorHandle 1474 + appBskyActorProfileByDid { 1475 + avatar { 1476 + url(preset: "avatar") 1477 + } 1478 + } 1401 1479 createdAt 1402 1480 } 1403 1481 } ··· 1442 1520 uri 1443 1521 did 1444 1522 actorHandle 1523 + appBskyActorProfileByDid { 1524 + avatar { 1525 + url(preset: "avatar") 1526 + } 1527 + } 1445 1528 body 1446 1529 parent 1447 1530 attachments { ··· 1773 1856 <span class="bug-card-title">${esc(bug.title)}</span> 1774 1857 </div> 1775 1858 <div class="bug-card-meta"> 1776 - @${esc(bug.actorHandle)} · ${formatTime(bug.createdAt)}${commentCount > 0 ? `<span class="comment-count"><i data-lucide="message-circle"></i> ${commentCount}</span>` : ""} 1859 + <span class="user-info">${renderAvatar(bug.appBskyActorProfileByDid, bug.actorHandle, "user-avatar-sm")}@${esc(bug.actorHandle)}</span> · ${formatTime(bug.createdAt)}${commentCount > 0 ? `<span class="comment-count"><i data-lucide="message-circle"></i> ${commentCount}</span>` : ""} 1777 1860 </div> 1778 1861 </div> 1779 1862 `; ··· 1875 1958 </div> 1876 1959 <div class="overlay-body"> 1877 1960 <div class="overlay-meta"> 1878 - <span>@${esc(bug.actorHandle)}</span> 1961 + <span class="user-info">${renderAvatar(bug.appBskyActorProfileByDid, bug.actorHandle)}@${esc(bug.actorHandle)}</span> 1879 1962 <span>·</span> 1880 1963 <span>${formatTime(bug.createdAt)}</span> 1881 1964 ${bug.appUsed ? `<span>·</span><span>${esc(bug.appUsed)}</span>` : ""} ··· 1943 2026 </div> 1944 2027 <div class="overlay-body"> 1945 2028 <div class="overlay-meta"> 1946 - <span>@${esc(bug.actorHandle)}</span> 2029 + <span class="user-info">${renderAvatar(bug.appBskyActorProfileByDid, bug.actorHandle)}@${esc(bug.actorHandle)}</span> 1947 2030 <span>·</span> 1948 2031 <span>${formatTime(bug.createdAt)}</span> 1949 2032 ${bug.appUsed ? `<span>·</span><span>${esc(bug.appUsed)}</span>` : ""} ··· 2342 2425 <div class="response-card"> 2343 2426 <div class="response-header"> 2344 2427 <span class="status-badge ${getStatusClass(response.status)}">${esc(response.status)}</span> 2345 - <span class="response-meta">@${esc(response.actorHandle)} · ${formatTime(response.createdAt)}</span> 2428 + <span class="response-meta"><span class="user-info">${renderAvatar(response.appBskyActorProfileByDid, response.actorHandle, "user-avatar-sm")}@${esc(response.actorHandle)}</span> · ${formatTime(response.createdAt)}</span> 2346 2429 ${isResponseAuthor ? `<button class="btn-icon btn-danger-text" onclick="handleDeleteResponse('${esc(response.uri)}')" title="Delete response">×</button>` : ""} 2347 2430 </div> 2348 2431 ${response.message ? `<p class="response-message">${esc(response.message)}</p>` : ""} ··· 2413 2496 return ` 2414 2497 <div class="comment-card" data-uri="${esc(comment.uri)}"> 2415 2498 <div class="comment-header"> 2416 - <span class="comment-meta">@${esc(comment.actorHandle)} · ${formatTime(comment.createdAt)}</span> 2499 + <span class="comment-meta"><span class="user-info">${renderAvatar(comment.appBskyActorProfileByDid, comment.actorHandle, "user-avatar-sm")}@${esc(comment.actorHandle)}</span> · ${formatTime(comment.createdAt)}</span> 2417 2500 ${canEdit ? ` 2418 2501 <div> 2419 2502 <button class="btn-icon" onclick="startEditComment('${esc(comment.uri)}')" title="Edit"><i data-lucide="pencil"></i></button> ··· 2896 2979 </div> 2897 2980 2898 2981 <div class="info-section"> 2982 + <h3>Comments</h3> 2983 + <p>Anyone logged in can add comments to discuss bugs, ask questions, or provide additional information. Comments support replies for threaded conversations and can include image attachments.</p> 2984 + </div> 2985 + 2986 + <div class="info-section"> 2899 2987 <h3>Bug Statuses</h3> 2900 2988 <ul class="status-list"> 2901 2989 <li><span class="status-badge status-open">Open</span> No response yet</li> ··· 2910 2998 <div class="info-section"> 2911 2999 <h3>Built on ATmosphere</h3> 2912 3000 <p>This bug tracker is built on the <a href="https://atproto.com/" target="_blank">AT Protocol</a>. Your bugs and responses are stored in your personal data repository, giving you ownership of your data.</p> 3001 + </div> 3002 + 3003 + <div class="info-section"> 3004 + <h3>Lexicons</h3> 3005 + <p>The bug tracker uses the following <a href="https://tangled.sh/slices.network/tools/tree/main/lexicons" target="_blank">lexicon schemas</a>:</p> 3006 + <div style="margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem;"> 3007 + <div> 3008 + <code style="font-weight: 600;">network.slices.tools.bug</code> 3009 + <div style="color: var(--text-secondary); font-size: 0.875rem;">Bug reports with title, description, severity, and attachments</div> 3010 + </div> 3011 + <div> 3012 + <code style="font-weight: 600;">network.slices.tools.bug.response</code> 3013 + <div style="color: var(--text-secondary); font-size: 0.875rem;">Official responses from namespace maintainers</div> 3014 + </div> 3015 + <div> 3016 + <code style="font-weight: 600;">network.slices.tools.bug.comment</code> 3017 + <div style="color: var(--text-secondary); font-size: 0.875rem;">Discussion comments with optional replies and attachments</div> 3018 + </div> 3019 + <div> 3020 + <code style="font-weight: 600;">network.slices.tools.bug.issue</code> 3021 + <div style="color: var(--text-secondary); font-size: 0.875rem;">Links between bugs and Tangled repository issues</div> 3022 + </div> 3023 + </div> 2913 3024 </div> 2914 3025 </div> 2915 3026 </div> ··· 3541 3652 3542 3653 const right = state.viewer 3543 3654 ? `<div class="user-status"> 3544 - <span>@${esc(state.viewer.handle)}</span> 3655 + ${renderAvatar(state.viewer.appBskyActorProfileByDid, state.viewer.handle, "user-avatar-xl user-avatar-ring")} 3545 3656 <button class="btn btn-secondary" onclick="logout()">Logout</button> 3546 3657 </div>` 3547 3658 : `<button class="btn btn-primary" onclick="login()">Login</button>`; ··· 3769 3880 try { 3770 3881 await initClient(); 3771 3882 if (await client.isAuthenticated()) { 3772 - const data = await client.query(`query { viewer { did handle } }`); 3883 + const data = await client.query(`query { viewer { did handle appBskyActorProfileByDid { avatar { url(preset: "avatar") } } } }`); 3773 3884 state.viewer = data?.viewer; 3774 3885 } else { 3775 3886 state.viewer = null;