fix(audit UI/UX): 44px slabs, missing grid base cols, aria-labels, dark weather panel, contrast, sheet width, stretched-link SpotCard

This commit is contained in:
SkateSpots 2026-10-05 21:47:29 +07:00
parent 2f6c6230f3
commit 9ca5f2a604
16 changed files with 37 additions and 25 deletions

View file

@ -801,7 +801,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}> <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button variant="ghost" size="icon" className="h-10 w-10 touch-manipulation active:scale-95 transition-transform"> <Button variant="ghost" size="icon" aria-label={t('chromeUi.menu')} className="h-10 w-10 touch-manipulation active:scale-95 transition-transform">
<Menu className="h-5 w-5" /> <Menu className="h-5 w-5" />
</Button> </Button>
</SheetTrigger> </SheetTrigger>

View file

@ -276,7 +276,7 @@ export function MarketplaceItemCard({ item, compact = false }: MarketplaceItemCa
{user && !isOwnListing && ( {user && !isOwnListing && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6 text-gray-400"> <Button variant="ghost" size="icon" className="min-h-0 min-w-0 h-6 w-6 text-gray-400">
<MoreVertical className="h-4 w-4" /> <MoreVertical className="h-4 w-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -299,7 +299,7 @@ export function MarketplaceItemCard({ item, compact = false }: MarketplaceItemCa
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-6 w-6 text-gray-400" className="min-h-0 min-w-0 h-6 w-6 text-gray-400"
disabled={isUpdatingStatus} disabled={isUpdatingStatus}
aria-label={t('marketUi.card.manageListing')} aria-label={t('marketUi.card.manageListing')}
> >

View file

@ -1625,6 +1625,12 @@ Instructions:
onPointerMove={onPointerMove} onPointerMove={onPointerMove}
onPointerUp={onPointerUp} onPointerUp={onPointerUp}
onClick={onBubbleClick} onClick={onBubbleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onBubbleClick(); }
}}
role="button"
tabIndex={0}
aria-label={t('sasquatchUi.openGuideAria')}
title={t('sasquatchUi.bubbleTitle')} title={t('sasquatchUi.bubbleTitle')}
> >
<div <div

View file

@ -61,7 +61,10 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
const isGone = status?.status === 'gone'; const isGone = status?.status === 'gone';
return ( return (
<Link to={route}> <div className="relative">
{/* Stretched link — the whole card navigates, without nesting interactive
elements (buttons, Zap, dropdowns) or dialogs inside an <a>. */}
<Link to={route} className="absolute inset-0 z-0" aria-label={spot.title} />
<Card className={cn( <Card className={cn(
"group overflow-hidden hover:shadow-2xl transition-all duration-300 border-2 bg-black/90 backdrop-blur", "group overflow-hidden hover:shadow-2xl transition-all duration-300 border-2 bg-black/90 backdrop-blur",
isShop isShop
@ -211,7 +214,7 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
)} )}
</div> </div>
<div className="flex items-center gap-2" onClick={(e) => e.preventDefault()}> <div className="relative z-10 flex items-center gap-2" onClick={(e) => e.preventDefault()}>
{user && !isShop && ( {user && !isShop && (
<Button <Button
size="icon" size="icon"
@ -277,7 +280,7 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
targetEventId={spot.id} targetEventId={spot.id}
targetTitle={spot.title} targetTitle={spot.title}
/> />
</Link> </div>
); );
} }

View file

@ -586,7 +586,7 @@ function createPopupContent(
// Weather section (if enabled and available) // Weather section (if enabled and available)
if (weatherData) { if (weatherData) {
const weatherSection = document.createElement('div'); const weatherSection = document.createElement('div');
weatherSection.className = 'mb-3 p-2 rounded-lg bg-gray-50 border border-gray-200'; weatherSection.className = 'mb-3 p-2 rounded-lg bg-white/[0.04] border border-white/10';
// Current conditions // Current conditions
const currentWeather = document.createElement('div'); const currentWeather = document.createElement('div');

View file

@ -169,7 +169,7 @@ export function VouchCard({
</Button> </Button>
)} )}
{isOwner && ( {isOwner && (
<p className="text-center text-[11px] text-gray-600"> <p className="text-center text-[11px] text-gray-400">
{t('socialUi.vouch.ownerNote')} {t('socialUi.vouch.ownerNote')}
</p> </p>
)} )}

View file

@ -44,7 +44,7 @@ export function LightningPrivacyTips({ className, defaultExpanded = false }: Lig
{isExpanded && ( {isExpanded && (
<CardContent className="space-y-4 pt-0"> <CardContent className="space-y-4 pt-0">
{/* Cashu vs Lightning Comparison */} {/* Cashu vs Lightning Comparison */}
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{/* Cashu - Recommended */} {/* Cashu - Recommended */}
<div className="p-3 rounded-lg bg-green-500/10 border border-green-500/30"> <div className="p-3 rounded-lg bg-green-500/10 border border-green-500/30">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">

View file

@ -26,6 +26,7 @@ const en = {
filter: 'Filter', filter: 'Filter',
sort: 'Sort', sort: 'Sort',
share: 'Share', share: 'Share',
more: 'More',
copy: 'Copy', copy: 'Copy',
copied: 'Copied!', copied: 'Copied!',
viewAll: 'View All', viewAll: 'View All',

View file

@ -27,6 +27,7 @@ const es = {
filter: 'Filtrar', filter: 'Filtrar',
sort: 'Ordenar', sort: 'Ordenar',
share: 'Compartir', share: 'Compartir',
more: 'Más',
copy: 'Copiar', copy: 'Copiar',
copied: '¡Copiado!', copied: '¡Copiado!',
viewAll: 'Ver Todo', viewAll: 'Ver Todo',

View file

@ -156,7 +156,7 @@ export default function AppShopPage() {
</span> </span>
</div> </div>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{PLANNED.slice(0, 2).map(({ icon: Icon, labelKey }) => ( {PLANNED.slice(0, 2).map(({ icon: Icon, labelKey }) => (
<div <div
key={labelKey} key={labelKey}
@ -195,7 +195,7 @@ export default function AppShopPage() {
</span> </span>
</div> </div>
<div className="mt-3 grid gap-3 sm:grid-cols-2"> <div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
{FREE_PRINT.map((item) => ( {FREE_PRINT.map((item) => (
<div <div
key={item.titleKey} key={item.titleKey}

View file

@ -432,7 +432,7 @@ export default function BecomeEditorPage() {
{image ? ( {image ? (
<div className="relative w-32 h-32"> <div className="relative w-32 h-32">
<img src={image} alt="profile" className="w-32 h-32 object-cover rounded-lg border-2 border-purple-500/40" /> <img src={image} alt="profile" className="w-32 h-32 object-cover rounded-lg border-2 border-purple-500/40" />
<Button type="button" variant="destructive" size="icon" className="absolute -top-2 -right-2 h-6 w-6" onClick={() => setImage(null)}> <Button type="button" variant="destructive" size="icon" className="min-h-0 min-w-0 absolute -top-2 -right-2 h-6 w-6" onClick={() => setImage(null)}>
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</Button> </Button>
</div> </div>

View file

@ -522,7 +522,7 @@ function FilterDrawer({
{t('common.clearAll')} {t('common.clearAll')}
</button> </button>
)} )}
<button onClick={onClose} className="w-7 h-7 flex items-center justify-center rounded-full bg-white/5 hover:bg-white/10 transition-colors"> <button onClick={onClose} className="min-h-0 min-w-0 w-7 h-7 flex items-center justify-center rounded-full bg-white/5 hover:bg-white/10 transition-colors">
<X className="h-3.5 w-3.5 text-gray-400" /> <X className="h-3.5 w-3.5 text-gray-400" />
</button> </button>
</div> </div>

View file

@ -139,7 +139,7 @@ export default function GlossaryPage() {
</p> </p>
</div> </div>
) : ( ) : (
<div className="grid gap-3 md:grid-cols-2"> <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
{searchResults.map((t) => ( {searchResults.map((t) => (
<div key={`${t.categoryId}-${t.name}`}> <div key={`${t.categoryId}-${t.name}`}>
<p className="mb-1 text-[10px] font-bold uppercase tracking-widest text-gray-600"> <p className="mb-1 text-[10px] font-bold uppercase tracking-widest text-gray-600">
@ -164,7 +164,7 @@ export default function GlossaryPage() {
<span className="text-xs text-gray-500">{t('settingsUi.glossary.tricksCount', { count: cat.tricks.length })}</span> <span className="text-xs text-gray-500">{t('settingsUi.glossary.tricksCount', { count: cat.tricks.length })}</span>
</div> </div>
<p className="mb-5 text-sm text-gray-400">{cat.blurb}</p> <p className="mb-5 text-sm text-gray-400">{cat.blurb}</p>
<div className="grid gap-3 md:grid-cols-2"> <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
{cat.tricks.map((t) => ( {cat.tricks.map((t) => (
<TrickRow key={t.name} trick={t} /> <TrickRow key={t.name} trick={t} />
))} ))}

View file

@ -171,7 +171,7 @@ function MiniStashCard({ item, onDelete }: { item: StashItem; onDelete: () => vo
</Badge> </Badge>
<button <button
onClick={onDelete} onClick={onDelete}
className="p-1 rounded hover:bg-red-500/20 text-gray-500 hover:text-red-400 transition-colors" className="min-h-0 min-w-0 p-1 rounded hover:bg-red-500/20 text-gray-500 hover:text-red-400 transition-colors"
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</button> </button>
@ -1144,7 +1144,7 @@ export default function MapPage() {
{/* View toggle — compact pill */} {/* View toggle — compact pill */}
<div className="flex items-center rounded-full border border-white/15 bg-black/50 backdrop-blur-md md:bg-white/4 md:border-white/10 p-0.5 flex-shrink-0"> <div className="flex items-center rounded-full border border-white/15 bg-black/50 backdrop-blur-md md:bg-white/4 md:border-white/10 p-0.5 flex-shrink-0">
<button <button
className={cn('h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90', className={cn('min-h-0 min-w-0 h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90',
viewMode === 'map' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')} viewMode === 'map' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')}
onClick={() => setViewMode('map')} onClick={() => setViewMode('map')}
aria-label={t('mapUi.mapViewAria')} aria-label={t('mapUi.mapViewAria')}
@ -1152,7 +1152,7 @@ export default function MapPage() {
<Map className="h-3.5 w-3.5" /> <Map className="h-3.5 w-3.5" />
</button> </button>
<button <button
className={cn('h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90', className={cn('min-h-0 min-w-0 h-8 w-8 rounded-full flex items-center justify-center transition-all touch-manipulation active:scale-90',
viewMode === 'list' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')} viewMode === 'list' ? 'bg-[#ff0040] shadow-lg shadow-[#ff0040]/30' : 'text-gray-500 hover:text-gray-300')}
onClick={() => setViewMode('list')} onClick={() => setViewMode('list')}
aria-label={t('mapUi.listViewAria')} aria-label={t('mapUi.listViewAria')}
@ -1496,7 +1496,7 @@ export default function MapPage() {
<span className="hidden xs:inline">{t('mapUi.stash')}</span> <span className="hidden xs:inline">{t('mapUi.stash')}</span>
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent side="right" className="w-[340px] sm:w-[400px] p-0 bg-black/95 border-l-2 border-[#ff6600]/30"> <SheetContent side="right" className="w-[min(340px,calc(100vw-2rem))] sm:w-[400px] p-0 bg-black/95 border-l-2 border-[#ff6600]/30">
<SheetHeader className="p-4 pb-0"> <SheetHeader className="p-4 pb-0">
<SheetTitle className="flex items-center gap-2 text-white"> <SheetTitle className="flex items-center gap-2 text-white">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#ff6600] to-[#ff0040] flex items-center justify-center"> <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#ff6600] to-[#ff0040] flex items-center justify-center">

View file

@ -603,7 +603,7 @@ function LiveIssuesSection() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<Skeleton className="h-56 w-full rounded-2xl" /> <Skeleton className="h-56 w-full rounded-2xl" />
<Skeleton className="h-56 w-full rounded-2xl" /> <Skeleton className="h-56 w-full rounded-2xl" />
</div> </div>
@ -615,7 +615,7 @@ function LiveIssuesSection() {
return ( return (
<section className="mb-16"> <section className="mb-16">
<SectionHeader emoji="📡" label={t('walletUi.newsletter.sectionLatestDrops')} color="#ccff00" /> <SectionHeader emoji="📡" label={t('walletUi.newsletter.sectionLatestDrops')} color="#ccff00" />
<div className="mt-4 grid gap-4 sm:grid-cols-2"> <div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
{issues.map((issue) => ( {issues.map((issue) => (
<LiveIssueCard key={issue.id} issue={issue} /> <LiveIssueCard key={issue.id} issue={issue} />
))} ))}
@ -709,7 +709,7 @@ export default function NewsletterPage() {
{t('walletUi.newsletter.supportUs')} {t('walletUi.newsletter.supportUs')}
</a> </a>
</p> </p>
<p className="text-[10px] text-gray-700 uppercase tracking-widest"> <p className="text-[10px] text-gray-400 uppercase tracking-widest">
{t('walletUi.newsletter.freeToSkate')} {t('walletUi.newsletter.freeToSkate')}
</p> </p>
</div> </div>

View file

@ -416,7 +416,7 @@ export default function SpotDetailPage() {
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
{isOwner && ( {isOwner && (
<Link to={`/edit-spot/${spot.pubkey}/${spot.identifier}`}> <Link to={`/edit-spot/${spot.pubkey}/${spot.identifier}`}>
<Button variant="outline" size="icon"> <Button variant="outline" size="icon" aria-label={t('common.edit')}>
<Pencil className="h-4 w-4" /> <Pencil className="h-4 w-4" />
</Button> </Button>
</Link> </Link>
@ -440,6 +440,7 @@ export default function SpotDetailPage() {
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
aria-label={t('common.save')}
onClick={() => setShowCollectionDialog(true)} onClick={() => setShowCollectionDialog(true)}
className="hover:bg-[#ccff00]/10 hover:text-[#ccff00] hover:border-[#ccff00]/50" className="hover:bg-[#ccff00]/10 hover:text-[#ccff00] hover:border-[#ccff00]/50"
> >
@ -464,7 +465,7 @@ export default function SpotDetailPage() {
spotTitle={spot.title} spotTitle={spot.title}
/> />
)} )}
<Button variant="outline" size="icon" onClick={handleShare}> <Button variant="outline" size="icon" onClick={handleShare} aria-label={t('common.share')}>
<Share2 className="h-4 w-4" /> <Share2 className="h-4 w-4" />
</Button> </Button>
<Button variant="outline" size="icon" onClick={handleDirections} aria-label={t('spotUi.getDirections')}> <Button variant="outline" size="icon" onClick={handleDirections} aria-label={t('spotUi.getDirections')}>
@ -473,7 +474,7 @@ export default function SpotDetailPage() {
{/* More options menu */} {/* More options menu */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="icon"> <Button variant="outline" size="icon" aria-label={t('common.more')}>
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>