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
+41 -12
View File
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import {
LayoutDashboard, Server, FlaskConical, Brain,
TrendingUp, Users, Database, Swords, DollarSign, Settings, Trophy, Medal,
PanelLeftClose, PanelLeftOpen,
} from 'lucide-react';
import { useGameStore, type ActivePage } from '@/store';
@@ -20,11 +21,20 @@ const NAV_ITEMS: { page: ActivePage; label: string; icon: typeof LayoutDashboard
{ page: 'settings', label: 'Settings', icon: Settings },
];
function getInitialCollapsed(): boolean {
try {
const stored = localStorage.getItem('ai-tycoon-sidebar-collapsed');
if (stored !== null) return stored === 'true';
return window.innerWidth < 1280;
} catch { return false; }
}
export function Sidebar() {
const activePage = useGameStore((s) => s.activePage);
const setActivePage = useGameStore((s) => s.setActivePage);
const companyName = useGameStore((s) => s.meta.companyName);
const era = useGameStore((s) => s.meta.currentEra);
const [collapsed, setCollapsed] = useState(getInitialCollapsed);
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
const currentEraIdx = eraOrder.indexOf(era);
@@ -55,13 +65,28 @@ export function Sidebar() {
});
};
const toggleCollapse = () => {
setCollapsed(prev => {
const next = !prev;
localStorage.setItem('ai-tycoon-sidebar-collapsed', String(next));
return next;
});
};
return (
<aside className="w-56 bg-surface-900 border-r border-surface-700 flex flex-col h-screen">
<div className="p-4 border-b border-surface-700">
<h1 className="text-lg font-bold text-accent-light truncate">{companyName}</h1>
<span className="text-xs text-surface-400 uppercase tracking-wider">
{era === 'startup' ? 'Startup' : era === 'scaleup' ? 'Scale-up' : era === 'bigtech' ? 'Big Tech' : 'AGI Era'}
</span>
<aside className={`${collapsed ? 'w-16' : 'w-56'} bg-surface-900 border-r border-surface-700 flex flex-col h-screen transition-all duration-200`}>
<div className={`${collapsed ? 'px-2 py-3' : 'p-4'} border-b border-surface-700 flex items-center ${collapsed ? 'justify-center' : 'justify-between'}`}>
{!collapsed && (
<div className="min-w-0">
<h1 className="text-lg font-bold text-accent-light truncate">{companyName}</h1>
<span className="text-xs text-surface-400 uppercase tracking-wider">
{era === 'startup' ? 'Startup' : era === 'scaleup' ? 'Scale-up' : era === 'bigtech' ? 'Big Tech' : 'AGI Era'}
</span>
</div>
)}
<button onClick={toggleCollapse} className="text-surface-400 hover:text-surface-200 shrink-0 p-1" aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}>
{collapsed ? <PanelLeftOpen size={18} /> : <PanelLeftClose size={18} />}
</button>
</div>
<nav className="flex-1 py-2 overflow-y-auto">
@@ -73,28 +98,32 @@ export function Sidebar() {
const showDivider = page === 'talent' || page === 'achievements';
return (
<div key={page}>
{showDivider && <div className="border-t border-surface-700 my-1 mx-4" />}
{showDivider && <div className={`border-t border-surface-700 my-1 ${collapsed ? 'mx-2' : 'mx-4'}`} />}
<button
onClick={() => handleNavClick(page)}
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm transition-colors ${
className={`w-full flex items-center ${collapsed ? 'justify-center px-2' : 'gap-3 px-4'} py-2.5 text-sm transition-colors ${
isActive
? 'bg-accent/10 text-accent-light border-r-2 border-accent'
: 'text-surface-300 hover:bg-surface-800 hover:text-surface-100'
}`}
title={collapsed ? label : undefined}
>
<Icon size={18} />
{label}
{isNew && (
{!collapsed && label}
{!collapsed && isNew && (
<span className="ml-auto text-[10px] font-bold bg-accent text-white px-1.5 py-0.5 rounded">NEW</span>
)}
{collapsed && isNew && (
<span className="absolute top-0 right-0 w-1.5 h-1.5 bg-accent rounded-full" />
)}
</button>
</div>
);
})}
</nav>
<div className="p-4 border-t border-surface-700 text-xs text-surface-500">
AI Tycoon v0.1
<div className={`${collapsed ? 'px-2 py-3 text-center' : 'p-4'} border-t border-surface-700 text-xs text-surface-500`}>
{collapsed ? 'v0.1' : 'AI Tycoon v0.1'}
</div>
</aside>
);