Harden accessibility: ARIA roles, semantic buttons, alt text, form labels

- Replace clickable divs with semantic <button> elements (NoteCard, SearchView)
- Add role="dialog", aria-modal, aria-labelledby on all modals (Login, Quote, Zap, Lightbox)
- Add role="presentation" on overlay backdrops (NoteCard menu, emoji pickers)
- Add aria-label to all icon-only buttons (close, nav arrows, context menu)
- Add aria-expanded to context menu toggle
- Add meaningful alt text to all 35+ images (avatars, covers, thumbnails, media)
- Add aria-label to form inputs (search, onboarding login)
This commit is contained in:
Jure
2026-04-02 17:01:02 +02:00
parent f1781691c5
commit 1fd613425d
27 changed files with 75 additions and 54 deletions

View File

@@ -71,7 +71,7 @@ export function ArticleCard({ event }: { event: NDKEvent }) {
{profile?.picture ? (
<img
src={profile.picture}
alt=""
alt={`${authorName}'s avatar`}
className="w-5 h-5 rounded-sm object-cover"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
/>
@@ -108,7 +108,7 @@ export function ArticleCard({ event }: { event: NDKEvent }) {
<div className="shrink-0 w-24 h-20 rounded-sm overflow-hidden bg-bg-raised">
<img
src={image}
alt=""
alt={`Cover image for ${title || "article"}`}
className="w-full h-full object-cover"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
/>

View File

@@ -43,7 +43,7 @@ function AuthorRow({ pubkey, publishedAt, readingTime }: { pubkey: string; publi
<div className="flex items-center gap-3 mb-6">
<button className="shrink-0" onClick={() => openProfile(pubkey)}>
{profile?.picture ? (
<img src={profile.picture} alt="" className="w-9 h-9 rounded-sm object-cover hover:opacity-80 transition-opacity"
<img src={profile.picture} alt={`${name}'s avatar`} className="w-9 h-9 rounded-sm object-cover hover:opacity-80 transition-opacity"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
) : (
<div className="w-9 h-9 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-sm">
@@ -361,7 +361,7 @@ export function ArticleView() {
<div className="mb-6 -mx-2">
<img
src={image}
alt=""
alt={`Cover image for ${title || "article"}`}
className="w-full aspect-video object-cover rounded-sm"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
/>

View File

@@ -56,7 +56,7 @@ function ConvRow({
}`}
>
{profile?.picture ? (
<img src={profile.picture} alt="" className="w-8 h-8 rounded-sm object-cover shrink-0"
<img src={profile.picture} alt={`${name}'s avatar`} className="w-8 h-8 rounded-sm object-cover shrink-0"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
) : (
<div className="w-8 h-8 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-xs shrink-0">
@@ -177,7 +177,7 @@ function ThreadPanel({
{/* Header */}
<div className="border-b border-border px-4 py-2.5 flex items-center gap-3 shrink-0">
{profile?.picture && (
<img src={profile.picture} alt="" className="w-7 h-7 rounded-sm object-cover" />
<img src={profile.picture} alt={`${name}'s avatar`} className="w-7 h-7 rounded-sm object-cover" />
)}
<button
onClick={() => openProfile(partnerPubkey)}

View File

@@ -213,7 +213,7 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () =
{avatar ? (
<img
src={avatar}
alt=""
alt="Your avatar"
className="w-9 h-9 rounded-sm object-cover bg-bg-raised"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
/>
@@ -252,7 +252,7 @@ export function ComposeBox({ onPublished, onNoteInjected }: { onPublished?: () =
) : (
<img
src={url}
alt=""
alt="Attachment preview"
className="h-16 w-auto rounded-sm border border-border object-cover"
onError={(e) => { (e.target as HTMLImageElement).className = "h-16 w-20 rounded-sm border border-border bg-bg-raised"; }}
/>

View File

@@ -68,7 +68,7 @@ export function FountainCard({ seg }: { seg: ContentSegment }) {
{episode.artworkUrl ? (
<img
src={episode.artworkUrl}
alt=""
alt={`${episode.title || "Episode"} artwork`}
className="w-12 h-12 rounded-sm object-cover shrink-0"
loading="lazy"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}

View File

@@ -175,7 +175,7 @@ export function InlineReplyBox({ event, name, rootEvent }: InlineReplyBoxProps)
) : (
<img
src={url}
alt=""
alt="Attachment preview"
className="h-12 w-auto rounded-sm border border-border object-cover"
onError={(e) => { (e.target as HTMLImageElement).className = "h-12 w-16 rounded-sm border border-border bg-bg-raised"; }}
/>

View File

@@ -83,7 +83,7 @@ export function YouTubeCard({ seg }: { seg: ContentSegment }) {
>
<img
src={`https://img.youtube.com/vi/${seg.mediaId}/hqdefault.jpg`}
alt=""
alt="Video thumbnail"
className="w-28 h-16 rounded-sm object-cover shrink-0"
loading="lazy"
/>

View File

@@ -120,7 +120,7 @@ export function NoteActions({ event, onReplyToggle, showReply }: NoteActionsProp
{/* Emoji picker popover */}
{showEmojiPicker && (
<>
<div className="fixed inset-0 z-[9]" onClick={() => setShowEmojiPicker(false)} />
<div className="fixed inset-0 z-[9]" role="presentation" onClick={() => setShowEmojiPicker(false)} />
<div className="absolute bottom-6 left-0 bg-bg-raised border border-border shadow-lg z-10 flex gap-0.5 px-1.5 py-1">
{REACTION_EMOJIS.map((emoji) => (
<button

View File

@@ -67,11 +67,11 @@ export function NoteCard({ event, focused, onReplyInThread }: NoteCardProps) {
>
<div className="flex gap-3">
{/* Avatar */}
<div className="shrink-0 cursor-pointer" onClick={() => openProfile(event.pubkey)}>
<button className="shrink-0 cursor-pointer" aria-label={`View profile of ${name}`} onClick={() => openProfile(event.pubkey)}>
{avatar ? (
<img
src={avatar}
alt=""
alt={`${name}'s avatar`}
className="w-9 h-9 rounded-sm object-cover bg-bg-raised hover:opacity-80 transition-opacity"
loading="lazy"
onError={(e) => {
@@ -83,7 +83,7 @@ export function NoteCard({ event, focused, onReplyInThread }: NoteCardProps) {
{name.charAt(0).toUpperCase()}
</div>
)}
</div>
</button>
{/* Content */}
<div className="min-w-0 flex-1">
@@ -103,14 +103,16 @@ export function NoteCard({ event, focused, onReplyInThread }: NoteCardProps) {
<div className="relative ml-auto">
<button
onClick={() => setMenuOpen((v) => !v)}
aria-label="More actions"
aria-expanded={menuOpen}
className="text-text-dim hover:text-text text-[14px] px-1 leading-none opacity-0 group-hover/card:opacity-100 transition-opacity"
>
</button>
{menuOpen && (
<>
<div className="fixed inset-0 z-[9]" onClick={() => setMenuOpen(false)} />
<div className="absolute right-0 top-5 bg-bg-raised border border-border shadow-lg z-10 w-32">
<div className="fixed inset-0 z-[9]" role="presentation" onClick={() => setMenuOpen(false)} />
<div role="menu" className="absolute right-0 top-5 bg-bg-raised border border-border shadow-lg z-10 w-32">
<button
onClick={() => { setMenuOpen(false); follows.includes(event.pubkey) ? unfollow(event.pubkey) : follow(event.pubkey); }}
className="w-full text-left px-3 py-2 text-[11px] text-text-muted hover:text-accent hover:bg-bg-hover transition-colors"

View File

@@ -23,7 +23,7 @@ function ImageGrid({ images, onImageClick }: { images: string[]; onImageClick: (
<div className="mt-2">
<img
src={images[0]}
alt=""
alt="Posted image"
loading="lazy"
className="max-w-full max-h-80 rounded-sm object-cover bg-bg-raised border border-border cursor-zoom-in"
onClick={(e) => { e.stopPropagation(); onImageClick(0); }}
@@ -40,7 +40,7 @@ function ImageGrid({ images, onImageClick }: { images: string[]; onImageClick: (
<img
key={idx}
src={src}
alt=""
alt="Posted image"
loading="lazy"
className="w-full aspect-[4/3] rounded-sm object-cover bg-bg-raised border border-border cursor-zoom-in"
onClick={(e) => { e.stopPropagation(); onImageClick(idx); }}
@@ -56,7 +56,7 @@ function ImageGrid({ images, onImageClick }: { images: string[]; onImageClick: (
<div className="mt-2 grid grid-cols-2 grid-rows-2 gap-1" style={{ gridTemplateRows: "1fr 1fr" }}>
<img
src={visible[0]}
alt=""
alt="Posted image"
loading="lazy"
className="w-full h-full rounded-sm object-cover bg-bg-raised border border-border cursor-zoom-in row-span-2"
style={{ aspectRatio: "3/4" }}
@@ -65,7 +65,7 @@ function ImageGrid({ images, onImageClick }: { images: string[]; onImageClick: (
/>
<img
src={visible[1]}
alt=""
alt="Posted image"
loading="lazy"
className="w-full aspect-[4/3] rounded-sm object-cover bg-bg-raised border border-border cursor-zoom-in"
onClick={(e) => { e.stopPropagation(); onImageClick(1); }}
@@ -73,7 +73,7 @@ function ImageGrid({ images, onImageClick }: { images: string[]; onImageClick: (
/>
<img
src={visible[2]}
alt=""
alt="Posted image"
loading="lazy"
className="w-full aspect-[4/3] rounded-sm object-cover bg-bg-raised border border-border cursor-zoom-in"
onClick={(e) => { e.stopPropagation(); onImageClick(2); }}
@@ -90,7 +90,7 @@ function ImageGrid({ images, onImageClick }: { images: string[]; onImageClick: (
<div key={idx} className="relative">
<img
src={src}
alt=""
alt="Posted image"
loading="lazy"
className="w-full aspect-[4/3] rounded-sm object-cover bg-bg-raised border border-border cursor-zoom-in"
onClick={(e) => { e.stopPropagation(); onImageClick(idx); }}
@@ -133,7 +133,7 @@ function QuotePreview({ eventId }: { eventId: string }) {
>
<div className="flex items-center gap-2 mb-1">
{profile?.picture && (
<img src={profile.picture} alt="" className="w-4 h-4 rounded-sm object-cover shrink-0"
<img src={profile.picture} alt={`${name}'s avatar`} className="w-4 h-4 rounded-sm object-cover shrink-0"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
)}
<span className="text-text-muted text-[11px] font-medium truncate">{name}</span>

View File

@@ -46,13 +46,16 @@ export function QuoteModal({ event, authorName, authorAvatar, onClose, onPublish
return (
<div
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
role="dialog"
aria-modal="true"
aria-labelledby="quote-modal-title"
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
<div className="bg-bg border border-border w-full max-w-md mx-4 shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h2 className="text-text text-sm font-medium">Quote note</h2>
<button onClick={onClose} className="text-text-dim hover:text-text text-lg leading-none">×</button>
<h2 id="quote-modal-title" className="text-text text-sm font-medium">Quote note</h2>
<button onClick={onClose} aria-label="Close" className="text-text-dim hover:text-text text-lg leading-none">×</button>
</div>
{/* Compose */}
@@ -72,7 +75,7 @@ export function QuoteModal({ event, authorName, authorAvatar, onClose, onPublish
<div className="border border-border px-3 py-2.5 bg-bg-raised rounded-sm">
<div className="flex items-center gap-2 mb-1.5">
{authorAvatar ? (
<img src={authorAvatar} alt="" className="w-4 h-4 rounded-sm object-cover" />
<img src={authorAvatar} alt={`${authorName}'s avatar`} className="w-4 h-4 rounded-sm object-cover" />
) : (
<div className="w-4 h-4 rounded-sm bg-accent/20 flex items-center justify-center text-accent text-[8px]">
{authorName.charAt(0).toUpperCase()}

View File

@@ -34,7 +34,7 @@ function FollowRow({
{avatar ? (
<img
src={avatar}
alt=""
alt={`${name}'s avatar`}
className="w-9 h-9 rounded-sm object-cover bg-bg-raised hover:opacity-80 transition-opacity"
loading="lazy"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}

View File

@@ -248,6 +248,7 @@ function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: ()
onChange={(e) => setValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={mode === "nsec" ? "nsec1…" : mode === "npub" ? "npub1…" : "bunker://…"}
aria-label={mode === "nsec" ? "Secret key" : mode === "npub" ? "Public key" : "Bunker URI"}
autoFocus
className="w-full bg-bg border border-border px-3 py-2 text-text text-[12px] font-mono focus:outline-none focus:border-accent/50 placeholder:text-text-dim mb-2"
style={{ WebkitUserSelect: "text", userSelect: "text" } as React.CSSProperties}

View File

@@ -72,7 +72,7 @@ export function EpisodeList({ show, onBack }: EpisodeListProps) {
{show.artworkUrl && (
<img
src={show.artworkUrl}
alt=""
alt={`${show.title || "Podcast"} artwork`}
className="w-20 h-20 rounded-sm object-cover shrink-0"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
/>

View File

@@ -16,7 +16,7 @@ export function PodcastCard({ show, onClick }: PodcastCardProps) {
{show.artworkUrl ? (
<img
src={show.artworkUrl}
alt=""
alt={`${show.title || "Podcast"} artwork`}
className="w-16 h-16 rounded-sm object-cover bg-bg"
loading="lazy"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}

View File

@@ -236,7 +236,7 @@ export function PodcastPlayerBar() {
{artwork && (
<img
src={artwork}
alt=""
alt="Now playing artwork"
className="w-10 h-10 rounded-sm object-cover shrink-0 bg-bg-raised"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
/>

View File

@@ -70,7 +70,7 @@ export function ProfileMediaGallery({ notes, loading }: { notes: NDKEvent[]; loa
>
<img
src={item.url}
alt=""
alt="Media content"
className="w-full h-full object-cover"
loading="lazy"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
@@ -88,7 +88,7 @@ export function ProfileMediaGallery({ notes, loading }: { notes: NDKEvent[]; loa
{item.thumbnailId ? (
<img
src={`https://img.youtube.com/vi/${item.thumbnailId}/mqdefault.jpg`}
alt=""
alt="Video thumbnail"
className="w-full h-full object-cover"
loading="lazy"
/>

View File

@@ -28,7 +28,7 @@ function TopFollowerAvatar({ pubkey }: { pubkey: string }) {
{profile?.picture ? (
<img
src={profile.picture}
alt=""
alt={`${name}'s avatar`}
className="w-5 h-5 rounded-sm object-cover bg-bg-raised"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
/>
@@ -218,7 +218,7 @@ export function ProfileView() {
)}
<img
src={profile.banner}
alt=""
alt={`${name}'s banner`}
className="w-full h-full object-cover object-[center_30%] cursor-pointer hover:opacity-90 transition-opacity"
onClick={() => setBannerLightbox(true)}
onLoad={() => setBannerLoaded(true)}
@@ -233,7 +233,7 @@ export function ProfileView() {
{avatar ? (
<img
src={avatar}
alt=""
alt={`${name}'s avatar`}
style={{ width: 64, height: 64 }}
className="rounded-sm object-cover bg-bg-raised"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}

View File

@@ -52,21 +52,21 @@ function UserRow({ user }: { user: ParsedUser }) {
return (
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-border hover:bg-bg-hover transition-colors">
<div className="shrink-0 cursor-pointer" onClick={() => navToProfile(user.pubkey)}>
<button className="shrink-0 cursor-pointer" aria-label={`View profile of ${displayName}`} onClick={() => navToProfile(user.pubkey)}>
{user.picture ? (
<img src={user.picture} alt="" className="w-9 h-9 rounded-sm object-cover bg-bg-raised"
<img src={user.picture} alt={`${displayName}'s avatar`} className="w-9 h-9 rounded-sm object-cover bg-bg-raised"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
) : (
<div className="w-9 h-9 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-xs">
{displayName.charAt(0).toUpperCase()}
</div>
)}
</div>
<div className="min-w-0 flex-1 cursor-pointer" onClick={() => navToProfile(user.pubkey)}>
</button>
<button className="min-w-0 flex-1 cursor-pointer text-left" onClick={() => navToProfile(user.pubkey)}>
<div className="text-text text-[13px] font-medium truncate">{displayName}</div>
{user.nip05 && <div className="text-text-dim text-[10px] truncate">{user.nip05}</div>}
{user.about && <div className="text-text-dim text-[11px] truncate mt-0.5">{user.about}</div>}
</div>
</button>
{loggedIn && !isOwn && (
<button
onClick={handleFollowToggle}
@@ -279,6 +279,7 @@ export function SearchView() {
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="search… try by:name, #tag, has:image, is:article, since:2026-01-01"
aria-label="Search Nostr"
autoFocus
className="flex-1 bg-transparent text-text text-[13px] placeholder:text-text-dim focus:outline-none"
/>
@@ -413,17 +414,17 @@ export function SearchView() {
)}
{visibleSuggestions.map((s) => s.profile && (
<div key={s.pubkey} className="flex items-center gap-3 px-4 py-2.5 border-b border-border hover:bg-bg-hover transition-colors group/suggestion">
<div className="shrink-0 cursor-pointer" onClick={() => useUIStore.getState().openProfile(s.pubkey)}>
<button className="shrink-0 cursor-pointer" aria-label={`View profile of ${s.profile.displayName || s.profile.name || "user"}`} onClick={() => useUIStore.getState().openProfile(s.pubkey)}>
{s.profile.picture ? (
<img src={s.profile.picture} alt="" className="w-9 h-9 rounded-sm object-cover bg-bg-raised"
<img src={s.profile.picture} alt={`${s.profile.displayName || s.profile.name || "User"}'s avatar`} className="w-9 h-9 rounded-sm object-cover bg-bg-raised"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
) : (
<div className="w-9 h-9 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-xs">
{(s.profile.displayName || s.profile.name || "?").charAt(0).toUpperCase()}
</div>
)}
</div>
<div className="min-w-0 flex-1 cursor-pointer" onClick={() => useUIStore.getState().openProfile(s.pubkey)}>
</button>
<button className="min-w-0 flex-1 cursor-pointer text-left" onClick={() => useUIStore.getState().openProfile(s.pubkey)}>
<div className="text-text text-[13px] font-medium truncate">
{s.profile.displayName || s.profile.name || shortenPubkey(s.pubkey)}
</div>
@@ -434,7 +435,7 @@ export function SearchView() {
{s.profile.about && (
<div className="text-text-dim text-[11px] truncate mt-0.5">{s.profile.about}</div>
)}
</div>
</button>
<SuggestionFollowButton pubkey={s.pubkey} />
<button
onClick={() => dismiss(s.pubkey)}

View File

@@ -18,7 +18,7 @@ export function EmojiPicker({ onSelect, onClose }: EmojiPickerProps) {
return (
<>
<div className="fixed inset-0 z-[9]" onClick={onClose} />
<div className="fixed inset-0 z-[9]" role="presentation" onClick={onClose} />
<div className="absolute bottom-7 right-0 bg-bg-raised border border-border shadow-lg z-10 w-64">
{/* Group tabs */}
<div className="flex border-b border-border">

View File

@@ -28,11 +28,15 @@ export function ImageLightbox({ images, index, onClose, onNavigate }: ImageLight
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-label="Image viewer"
onClick={onClose}
>
{/* Close button */}
<button
onClick={onClose}
aria-label="Close image viewer"
className="absolute top-4 right-4 text-white/70 hover:text-white text-[20px] w-8 h-8 flex items-center justify-center transition-colors z-10"
>
@@ -49,6 +53,7 @@ export function ImageLightbox({ images, index, onClose, onNavigate }: ImageLight
{hasPrev && (
<button
onClick={(e) => { e.stopPropagation(); onNavigate(index - 1); }}
aria-label="Previous image"
className="absolute left-4 top-1/2 -translate-y-1/2 text-white/50 hover:text-white text-[28px] w-10 h-10 flex items-center justify-center transition-colors z-10"
>
@@ -58,7 +63,7 @@ export function ImageLightbox({ images, index, onClose, onNavigate }: ImageLight
{/* Image */}
<img
src={images[index]}
alt=""
alt={`Image ${index + 1} of ${images.length}`}
className="max-w-[90vw] max-h-[90vh] object-contain select-none"
onClick={(e) => e.stopPropagation()}
draggable={false}
@@ -68,6 +73,7 @@ export function ImageLightbox({ images, index, onClose, onNavigate }: ImageLight
{hasNext && (
<button
onClick={(e) => { e.stopPropagation(); onNavigate(index + 1); }}
aria-label="Next image"
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/50 hover:text-white text-[28px] w-10 h-10 flex items-center justify-center transition-colors z-10"
>

View File

@@ -110,7 +110,11 @@ export function LoginModal({ onClose }: LoginModalProps) {
return (
<div
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
role="dialog"
aria-modal="true"
aria-labelledby="login-modal-title"
onClick={onClose}
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
>
<div
className="bg-bg-raised border border-border w-full max-w-md mx-4"
@@ -118,9 +122,10 @@ export function LoginModal({ onClose }: LoginModalProps) {
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<h2 className="text-text text-sm font-medium">Login</h2>
<h2 id="login-modal-title" className="text-text text-sm font-medium">Login</h2>
<button
onClick={onClose}
aria-label="Close login dialog"
className="text-text-dim hover:text-text text-lg leading-none"
>
×

View File

@@ -27,7 +27,7 @@ function MutedRow({ pubkey, onUnmute }: { pubkey: string; onUnmute: () => void }
return (
<div className="flex items-center gap-3 px-3 py-2 border border-border text-[12px] group">
{profile?.picture && (
<img src={profile.picture} alt="" className="w-5 h-5 rounded-sm object-cover shrink-0" />
<img src={profile.picture} alt={`${name}'s avatar`} className="w-5 h-5 rounded-sm object-cover shrink-0" />
)}
<span className="text-text truncate flex-1">{name}</span>
<button

View File

@@ -10,7 +10,7 @@ function Avatar({ account, size = "w-6 h-6", textSize = "text-[10px]" }: { accou
return (
<img
src={account.picture}
alt=""
alt={`${account.name || "Account"} avatar`}
className={`${size} rounded-sm object-cover shrink-0`}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";

View File

@@ -21,7 +21,7 @@ function AncestorCard({ event }: { event: NDKEvent }) {
>
<div className="shrink-0 mt-0.5">
{avatar ? (
<img src={avatar} alt="" className="w-6 h-6 rounded-sm object-cover bg-bg-raised" loading="lazy" />
<img src={avatar} alt={`${name}'s avatar`} className="w-6 h-6 rounded-sm object-cover bg-bg-raised" loading="lazy" />
) : (
<div className="w-6 h-6 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-[9px]">
{name.charAt(0).toUpperCase()}

View File

@@ -95,7 +95,7 @@ function ZapRow({
onClick={() => pubkey && openProfile(pubkey)}
>
{avatar ? (
<img src={avatar} alt="" className="w-8 h-8 rounded-sm object-cover" loading="lazy"
<img src={avatar} alt={`${name}'s avatar`} className="w-8 h-8 rounded-sm object-cover" loading="lazy"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
) : (
<div className="w-8 h-8 rounded-sm bg-bg-raised border border-border flex items-center justify-center text-text-dim text-xs">

View File

@@ -45,6 +45,9 @@ export function ZapModal({ target, recipientName, onClose }: ZapModalProps) {
return (
<div
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
role="dialog"
aria-modal="true"
aria-label={`Zap ${recipientName}`}
onClick={handleBackdrop}
>
<div className="bg-bg border border-border w-80 shadow-2xl">
@@ -54,7 +57,7 @@ export function ZapModal({ target, recipientName, onClose }: ZapModalProps) {
<div className="text-text text-[13px] font-medium"> Zap {recipientName}</div>
{!nwcUri && <div className="text-danger text-[10px] mt-0.5">No wallet connected</div>}
</div>
<button onClick={onClose} className="text-text-dim hover:text-text text-[11px] transition-colors"></button>
<button onClick={onClose} aria-label="Close" className="text-text-dim hover:text-text text-[11px] transition-colors"></button>
</div>
{/* No wallet state */}