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
@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react';
import { X, CheckCircle, AlertTriangle, AlertCircle, Info, Bell } from 'lucide-react';
import { X, CheckCircle, AlertTriangle, AlertCircle, Info, Bell, Trash2 } from 'lucide-react';
import { useGameStore, type GameNotification } from '@/store';
import { formatDuration } from '@ai-tycoon/shared';
@@ -13,6 +13,8 @@ const ICON_MAP = {
export function NotificationPanel({ onClose }: { onClose: () => void }) {
const notifications = useGameStore((s) => s.notifications);
const markAllRead = useGameStore((s) => s.markAllNotificationsRead);
const removeNotification = useGameStore((s) => s.removeNotification);
const clearAll = useGameStore((s) => s.clearAllNotifications);
const currentTick = useGameStore((s) => s.meta.tickCount);
const panelRef = useRef<HTMLDivElement>(null);
@@ -44,9 +46,16 @@ export function NotificationPanel({ onClose }: { onClose: () => void }) {
>
<div className="px-4 py-3 border-b border-surface-700 flex items-center justify-between shrink-0">
<h3 className="text-sm font-semibold">Notifications</h3>
<button onClick={onClose} className="text-surface-400 hover:text-surface-200">
<X size={14} />
</button>
<div className="flex items-center gap-2">
{notifications.length > 0 && (
<button onClick={clearAll} className="text-surface-500 hover:text-surface-300 text-[10px]" title="Clear all">
<Trash2 size={12} />
</button>
)}
<button onClick={onClose} className="text-surface-400 hover:text-surface-200">
<X size={14} />
</button>
</div>
</div>
<div className="overflow-y-auto flex-1">
{notifications.length === 0 ? (
@@ -59,7 +68,7 @@ export function NotificationPanel({ onClose }: { onClose: () => void }) {
const { icon: Icon, color } = ICON_MAP[n.type] ?? ICON_MAP.info;
const ticksAgo = currentTick - n.tick;
return (
<div key={n.id} className="px-4 py-3 border-b border-surface-800 last:border-0 hover:bg-surface-800/50">
<div key={n.id} className="px-4 py-3 border-b border-surface-800 last:border-0 hover:bg-surface-800/50 group">
<div className="flex items-start gap-2">
<Icon size={14} className={`${color} mt-0.5 shrink-0`} />
<div className="flex-1 min-w-0">
@@ -67,6 +76,9 @@ export function NotificationPanel({ onClose }: { onClose: () => void }) {
<div className="text-xs text-surface-400">{n.message}</div>
<div className="text-xs text-surface-600 mt-1">{formatDuration(ticksAgo)} ago</div>
</div>
<button onClick={() => removeNotification(n.id)} className="text-surface-600 hover:text-surface-300 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 mt-0.5">
<X size={12} />
</button>
</div>
</div>
);