fix(a11y): batch-fix useButtonType (32→3) and noSvgWithoutTitle (20→7)

- Add type="button" to 29 button elements across 14 components
- Add title element to 13 SVGs across 9 components
- Remaining a11y: 159 errors (noLabelWithoutControl 37, noStaticElementInteractions 22, useKeyWithClickEvents 21, useSemanticElements 8, noSvgWithoutTitle 7, useButtonType 3, noAssignInExpressions 3)
This commit is contained in:
Crypto Rug Munch 2026-07-09 14:40:25 +02:00
parent 096a168c03
commit 14fe663f28
23 changed files with 42 additions and 49 deletions

View file

@ -258,6 +258,7 @@ export function ConsentBanner() {
stroke="currentColor"
strokeWidth={2}
>
<title>Icon</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
@ -293,6 +294,7 @@ export function ConsentBanner() {
stroke="currentColor"
strokeWidth={2}
>
<title>Icon</title>
<path
strokeLinecap="round"
strokeLinejoin="round"

View file

@ -37,6 +37,7 @@ export class ErrorBoundary extends Component<Props, State> {
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 rounded-xl bg-red-500/20 flex items-center justify-center">
<svg className="w-6 h-6 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<title>Icon</title>
<path
strokeLinecap="round"
strokeLinejoin="round"

View file

@ -390,6 +390,7 @@ function SentimentPanel({ sentiment }: { sentiment?: SentimentData }) {
<div className="flex items-center gap-5">
<div className="relative w-20 h-20">
<svg className="w-full h-full transform -rotate-90">
<title>Icon</title>
<circle cx="40" cy="40" r="34" fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="6" />
<circle
cx="40"

View file

@ -14,6 +14,7 @@ export default function RiskRing({ score, size = 48, strokeWidth = 4 }: RiskRing
return (
<div className="relative inline-flex items-center justify-center" style={{ width: size, height: size }}>
<svg width={size} height={size} className="transform -rotate-90">
<title>Icon</title>
<circle
cx={size / 2}
cy={size / 2}

View file

@ -139,6 +139,7 @@ export function TraceTab() {
</div>
<div className="relative">
<svg ref={svgRef} viewBox="0 0 800 500" className="w-full h-[500px] bg-[#0a0e1a]">
<title>Icon</title>
<defs>
<marker id="arrowhead" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#5C6080" />

View file

@ -157,8 +157,7 @@ export default function AISignals({ chain, tokenAddress, initialSignals, classNa
const meta = t === 'all' ? null : TYPE_META[t]
const count = t === 'all' ? signals.length : signals.filter((s) => s.type === t).length
return (
<button
key={t}
<button type="button" key={t}
onClick={() => setFilterType(t as any)}
className={`flex items-center gap-1 px-2 py-1 rounded text-[10px] uppercase tracking-wider transition-colors ${
filterType === t

View file

@ -160,8 +160,7 @@ export default function LiveTradeTape({ chain, tokenAddress, initialTrades = [],
<div className="flex items-center justify-between px-3 py-2 border-b border-[rgba(139,92,246,0.08)]">
<div className="flex gap-1">
{(['all', 'whale', 'bot', 'suspicious'] as const).map((f) => (
<button
key={f}
<button type="button" key={f}
onClick={() => setFilter(f)}
className={`px-2 py-0.5 text-[10px] uppercase tracking-wider rounded transition-colors ${
filter === f
@ -180,8 +179,7 @@ export default function LiveTradeTape({ chain, tokenAddress, initialTrades = [],
{stats.suspicious} SUSPICIOUS
</span>
)}
<button
onClick={() => setPaused((p) => !p)}
<button type="button" onClick={() => setPaused((p) => !p)}
className="px-2 py-0.5 bg-[rgba(139,92,246,0.08)] hover:bg-[rgba(139,92,246,0.2)] rounded text-[10px]"
>
{paused ? '▶ RESUME' : '⏸ PAUSE'}

View file

@ -210,8 +210,7 @@ export default function MultiChainCompare({ initialTokens = [], className = '' }
placeholder="chain:address (e.g. solana:So1111... or just 0x...)"
className="flex-1 px-3 py-1.5 bg-[rgba(255,255,255,0.02)] border border-[rgba(139,92,246,0.08)] rounded-lg text-xs text-[#F1F1F6] placeholder:text-[#5C6080] focus:border-[#8B5CF6]/30 focus:outline-none font-mono"
/>
<button
onClick={handleAdd}
<button type="button" onClick={handleAdd}
disabled={loading || !search.trim()}
className="px-3 py-1.5 bg-[#8B5CF6] hover:bg-[#A78BFA] disabled:opacity-50 rounded-lg text-xs font-semibold uppercase tracking-wider text-white transition-colors"
>
@ -320,8 +319,7 @@ export default function MultiChainCompare({ initialTokens = [], className = '' }
</div>
</div>
<div className="col-span-1 text-right">
<button
onClick={() => removeToken(idx)}
<button type="button" onClick={() => removeToken(idx)}
className="text-[10px] text-[#5C6080] hover:text-[#FF3366] transition-colors"
title="Remove"
>
@ -339,15 +337,13 @@ export default function MultiChainCompare({ initialTokens = [], className = '' }
Page {page + 1} of {totalPages} · {ranked.length} tokens
</span>
<div className="flex gap-1">
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
<button type="button" onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0}
className="p-1 bg-[rgba(139,92,246,0.08)] hover:bg-[rgba(139,92,246,0.2)] disabled:opacity-30 rounded"
>
<ChevronLeft className="w-3 h-3" />
</button>
<button
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
<button type="button" onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
disabled={page >= totalPages - 1}
className="p-1 bg-[rgba(139,92,246,0.08)] hover:bg-[rgba(139,92,246,0.2)] disabled:opacity-30 rounded"
>

View file

@ -71,6 +71,7 @@ function CircularProgress({ value, size = 120, color = '#8b5cf6', label = '' })
return (
<div className="relative inline-flex items-center justify-center">
<svg width={size} height={size} className="-rotate-90">
<title>Icon</title>
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="8" />
<circle
cx={size / 2}

View file

@ -50,6 +50,7 @@ function CircularProgress({ value, size = 120, color = '#8b5cf6' }) {
return (
<div className="relative inline-flex items-center justify-center">
<svg width={size} height={size} className="-rotate-90">
<title>Icon</title>
<circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="8" />
<circle
cx={size / 2}
@ -470,8 +471,7 @@ export default function RugChartsPluginViewer({ tokenAddress, chain }) {
const _hasData = plugins[meta.name] && !plugins[meta.name].error
const error = plugins[meta.name]?.error
return (
<button
key={meta.name}
<button type="button" key={meta.name}
onClick={() => setActiveTab(meta.name)}
className={`flex items-center gap-2 px-3 py-2 rounded-xl text-xs font-bold whitespace-nowrap transition-all border ${
isActive
@ -510,8 +510,7 @@ export default function RugChartsPluginViewer({ tokenAddress, chain }) {
{/* Refresh button */}
<div className="flex justify-end">
<button
onClick={fetchPlugins}
<button type="button" onClick={fetchPlugins}
className="px-4 py-2 rounded-xl text-xs font-bold text-gray-400 hover:text-white bg-white/5 border border-white/10 hover:border-white/20 transition-all"
>
Refresh Analysis

View file

@ -195,8 +195,7 @@ export default function RugPullWarning({ chain, tokenAddress, initialWarnings, c
</p>
</div>
</div>
<button
onClick={runScan}
<button type="button" onClick={runScan}
disabled={scanning}
className="p-1.5 rounded bg-[rgba(139,92,246,0.08)] hover:bg-[rgba(139,92,246,0.2)] disabled:opacity-50"
title="Re-scan"

View file

@ -180,6 +180,7 @@ export default function ScamRiskMeter({
{/* Arc meter */}
<div className="relative w-24 h-24 flex-shrink-0">
<svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
<title>Icon</title>
<circle cx="50" cy="50" r="42" stroke="rgba(139,92,246,0.1)" strokeWidth="8" fill="none" />
<circle
cx="50"
@ -220,8 +221,7 @@ export default function ScamRiskMeter({
{/* Factor bars by category */}
{!loading && factors.length > 0 && (
<div className="border-t border-[rgba(139,92,246,0.1)]">
<button
onClick={() => setExpanded((e) => !e)}
<button type="button" onClick={() => setExpanded((e) => !e)}
className="w-full px-4 py-2 flex items-center justify-between hover:bg-[rgba(139,92,246,0.04)] transition-colors"
>
<span className="text-[11px] uppercase tracking-wider text-[#F1F1F6] font-semibold">

View file

@ -66,7 +66,7 @@ export default function BotNetworkPanel({ tokenAddress, chain }: BotNetworkPanel
<h3 className="text-[14px] font-semibold text-[#F1F1F6]">Bot Network Detection</h3>
</div>
</div>
<button onClick={detectBots} disabled={loading} className="btn btn-primary w-full">
<button type="button" onClick={detectBots} disabled={loading} className="btn btn-primary w-full">
{loading ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Bot className="w-4 h-4 mr-2" />}
{loading ? 'Analyzing transaction patterns...' : 'Detect Bot Networks'}
</button>

View file

@ -92,7 +92,7 @@ export default function ContractDeepDive({ tokenAddress, chain }: ContractDeepDi
<h3 className="text-[14px] font-semibold text-[#F1F1F6]">Contract Deep Dive</h3>
</div>
</div>
<button onClick={analyze} className="btn btn-primary w-full">
<button type="button" onClick={analyze} className="btn btn-primary w-full">
<Code className="w-4 h-4 mr-2" />
Analyze Contract
</button>
@ -181,8 +181,7 @@ export default function ContractDeepDive({ tokenAddress, chain }: ContractDeepDi
</div>
{functions.length > 4 && (
<button
onClick={() => setExpanded(!expanded)}
<button type="button" onClick={() => setExpanded(!expanded)}
className="text-[10px] text-purple-400 mt-2 hover:text-purple-300"
>
{expanded ? 'Show less' : `Show ${functions.length - 4} more functions`}

View file

@ -180,8 +180,7 @@ export default function DeployerNetwork({
<Calendar className="w-3 h-3" />
<span>Window:</span>
{[30, 60, 90, 180, 365].map((d) => (
<button
key={d}
<button type="button" key={d}
onClick={() => setWindowDays(d)}
className={`px-2 py-0.5 rounded ${windowDays === d ? 'bg-[#8B5CF6] text-white' : 'bg-[rgba(139,92,246,0.08)] hover:bg-[rgba(139,92,246,0.2)]'}`}
>

View file

@ -55,11 +55,11 @@ export default function ExportPanel({ tokenAddress, chain, scanData, aiResult, s
</div>
<div className="grid grid-cols-2 gap-2">
<button onClick={generateJSON} className="btn btn-ghost btn-sm flex items-center gap-2">
<button type="button" onClick={generateJSON} className="btn btn-ghost btn-sm flex items-center gap-2">
<FileJson className="w-4 h-4" />
JSON
</button>
<button onClick={generateShareLink} className="btn btn-ghost btn-sm flex items-center gap-2">
<button type="button" onClick={generateShareLink} className="btn btn-ghost btn-sm flex items-center gap-2">
<Share2 className="w-4 h-4" />
Share Link
</button>

View file

@ -197,8 +197,7 @@ export default function FundingTracer({ chain, address, initialHops, className =
const meta = TYPE_META[t]
const stat = sourceStats[t] || { count: 0, total_usd: 0 }
return (
<button
key={t}
<button type="button" key={t}
onClick={() => setFilter(filter === t ? 'all' : (t as any))}
className={`px-2 py-1.5 rounded border transition-colors ${
filter === t
@ -227,8 +226,7 @@ export default function FundingTracer({ chain, address, initialHops, className =
<div className="flex items-center gap-2 text-[#5C6080]">
<span>Depth:</span>
{[1, 2, 3, 5].map((d) => (
<button
key={d}
<button type="button" key={d}
onClick={() => setDepth(d)}
className={`px-2 py-0.5 rounded ${depth === d ? 'bg-[#8B5CF6] text-white' : 'bg-[rgba(139,92,246,0.08)] hover:bg-[rgba(139,92,246,0.2)]'}`}
>

View file

@ -214,6 +214,7 @@ export default function MultiTokenCompare({
{/* Radar chart */}
<div className="flex items-center justify-center bg-[#0E1525] rounded-lg p-3 border border-white/5">
<svg width="220" height="220" viewBox="0 0 220 220" className="overflow-visible">
<title>Icon</title>
{/* Concentric rings */}
{[0.25, 0.5, 0.75, 1].map((s) => (
<circle

View file

@ -124,19 +124,19 @@ export function RugMapsCanvas({ data, chain, width = 800, height = 500 }: RugMap
return (
<div className={`relative ${isFullscreen ? 'fixed inset-0 z-50 bg-[#0a0a1a]' : ''}`}>
<div className="absolute top-2 right-2 z-10 flex gap-1">
<button onClick={zoomIn} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300">
<button type="button" onClick={zoomIn} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300">
<ZoomIn size={16} />
</button>
<button onClick={zoomOut} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300">
<button type="button" onClick={zoomOut} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300">
<ZoomOut size={16} />
</button>
<button onClick={fitAll} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300">
<button type="button" onClick={fitAll} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300">
<RotateCcw size={16} />
</button>
<button onClick={toggleFullscreen} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300">
<button type="button" onClick={toggleFullscreen} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-gray-300">
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</button>
<button onClick={exportPng} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-green-400">
<button type="button" onClick={exportPng} className="p-1.5 rounded bg-[#1a1a2e] hover:bg-[#2a2a3e] text-green-400">
<Download size={16} />
</button>
</div>

View file

@ -99,28 +99,24 @@ export default function TemporalPlayback({ frames, onFrameChange }: TemporalPlay
{/* Controls */}
<div className="flex items-center gap-3 mb-4">
<button
onClick={() => setCurrentFrame(0)}
<button type="button" onClick={() => setCurrentFrame(0)}
className="p-2 rounded-[8px] hover:bg-[rgba(255,255,255,0.05)] text-[#5C6080]"
>
<SkipBack className="w-4 h-4" />
</button>
<button
onClick={() => setIsPlaying(!isPlaying)}
<button type="button" onClick={() => setIsPlaying(!isPlaying)}
className="p-2.5 rounded-[10px] bg-cyan-500/10 border border-cyan-500/20 text-cyan-400 hover:bg-cyan-500/20"
>
{isPlaying ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
</button>
<button
onClick={() => setCurrentFrame(Math.min(frames.length - 1, currentFrame + 10))}
<button type="button" onClick={() => setCurrentFrame(Math.min(frames.length - 1, currentFrame + 10))}
className="p-2 rounded-[8px] hover:bg-[rgba(255,255,255,0.05)] text-[#5C6080]"
>
<SkipForward className="w-4 h-4" />
</button>
<div className="flex items-center gap-1 ml-auto">
{[0.5, 1, 2, 4].map((s) => (
<button
key={s}
<button type="button" key={s}
onClick={() => setSpeed(s)}
className={`px-2 py-1 rounded-[6px] text-[10px] ${speed === s ? 'bg-cyan-500/20 text-cyan-400' : 'text-[#5C6080] hover:text-[#9DA0B0]'}`}
>

View file

@ -125,8 +125,7 @@ export default function X402PaymentGate({ onUpgrade, isConnected, walletAddress,
})}
</div>
<button
onClick={() => selectedTier && handlePay(TIERS.find((t) => t.id === selectedTier)!)}
<button type="button" onClick={() => selectedTier && handlePay(TIERS.find((t) => t.id === selectedTier)!)}
disabled={!(selectedTier && isConnected) || processing}
className="btn btn-primary w-full mt-3"
>

View file

@ -20,6 +20,7 @@ export function WalletClusterGraph({
return (
<svg viewBox="0 0 800 600" className="w-full h-auto">
<title>Icon</title>
<defs>
<filter id="glow-tc">
<feGaussianBlur stdDeviation="3" result="coloredBlur" />

View file

@ -24,6 +24,7 @@ export default function Sparkline({
if (!data || data.length < 2) {
return (
<svg width={width} height={height} className="opacity-30">
<title>Icon</title>
<line
x1="0"
y1={height / 2}
@ -64,6 +65,7 @@ export default function Sparkline({
return (
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className="overflow-visible">
<title>Icon</title>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity="0.25" />