Comprehensive UX audit fixes: navigation, feedback, affordances, and accessibility
CI / build-and-push (push) Successful in 28s

Address 18 issues across high/medium/low impact tiers identified in a full
interface review. Key changes: Models page decomposed into tabs, confirmation
dialogs for irreversible actions (deploy/open-source/acquire), chart Y-axes
made visible, hash router extended for Market tab persistence, collapsible
sidebar, keyboard navigation shortcuts (g+key chords), notification bulk
actions, achievement progress bars, and ARIA label improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 09:05:26 -04:00
parent 09a5cb69a7
commit 8d650fefae
17 changed files with 332 additions and 82 deletions
+22 -2
View File
@@ -1,5 +1,8 @@
import { useState } from 'react';
import { Swords, TrendingUp, Shield, Users, Brain, ShoppingCart } from 'lucide-react';
import { useGameStore } from '@/store';
import { ConfirmModal } from '@/components/common/ConfirmModal';
import { Tooltip } from '@/components/common/Tooltip';
import { formatMoney, formatNumber } from '@ai-tycoon/shared';
import type { Era } from '@ai-tycoon/shared';
@@ -27,6 +30,7 @@ export function CompetitorsPage() {
const money = useGameStore((s) => s.economy.money);
const acquireCompetitor = useGameStore((s) => s.acquireCompetitor);
const canAcquire = (era: Era) => era === 'bigtech' || era === 'agi';
const [acquireConfirm, setAcquireConfirm] = useState<{ id: string; name: string; cost: number } | null>(null);
return (
<div className="space-y-6">
@@ -55,10 +59,15 @@ export function CompetitorsPage() {
key={rival.id}
className="absolute top-0 h-full w-0.5 bg-danger"
style={{ left: `${rival.estimatedCapability}%` }}
title={`${rival.name}: ${rival.estimatedCapability.toFixed(1)}`}
/>
))}
</div>
<div className="flex items-center gap-4 mt-1.5 text-[10px]">
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-accent inline-block" />You: {playerBest.toFixed(1)}</span>
{rivals.filter(r => r.status === 'active').map(rival => (
<span key={rival.id} className="flex items-center gap-1"><span className="w-2 h-0.5 bg-danger inline-block" />{rival.name}: {rival.estimatedCapability.toFixed(1)}</span>
))}
</div>
</div>
</div>
</div>
@@ -84,7 +93,7 @@ export function CompetitorsPage() {
return (
<div className="text-right">
<button
onClick={() => acquireCompetitor(rival.id)}
onClick={() => setAcquireConfirm({ id: rival.id, name: rival.name, cost })}
disabled={money < cost}
className="flex items-center gap-1 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border border-blue-600/30 rounded px-3 py-1.5 text-xs disabled:opacity-40 disabled:cursor-not-allowed"
>
@@ -134,6 +143,17 @@ export function CompetitorsPage() {
<p>No competitors detected yet.</p>
</div>
)}
{acquireConfirm && (
<ConfirmModal
title="Acquire Competitor"
message={`Acquire "${acquireConfirm.name}" for ${formatMoney(acquireConfirm.cost)}? This will absorb their users and technology. This cannot be undone.`}
confirmLabel={`Acquire (${formatMoney(acquireConfirm.cost)})`}
danger
onConfirm={() => { acquireCompetitor(acquireConfirm.id); setAcquireConfirm(null); }}
onCancel={() => setAcquireConfirm(null)}
/>
)}
</div>
);
}