Comprehensive UX audit fixes: navigation, feedback, affordances, and accessibility
CI / build-and-push (push) Successful in 28s
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:
@@ -1,10 +1,12 @@
|
||||
import { useGameStore } from '@/store';
|
||||
import { ACHIEVEMENT_DEFINITIONS } from '@ai-tycoon/game-engine';
|
||||
import { formatNumber } from '@ai-tycoon/shared';
|
||||
import {
|
||||
Trophy, Lock, Server, Brain, Rocket, DollarSign, Sprout, Users,
|
||||
Globe, Sparkles, TrendingUp, Building2, Atom, Cpu, FlaskConical,
|
||||
GitBranch, Zap,
|
||||
} from 'lucide-react';
|
||||
import type { AchievementCondition } from '@ai-tycoon/shared';
|
||||
|
||||
const ICON_MAP: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
|
||||
Trophy, Server, Brain, Rocket, DollarSign, Sprout, Users,
|
||||
@@ -12,9 +14,34 @@ const ICON_MAP: Record<string, React.ComponentType<{ size?: number; className?:
|
||||
GitBranch, Zap,
|
||||
};
|
||||
|
||||
function resolveField(state: Record<string, unknown>, path: string): number {
|
||||
const parts = path.split('.');
|
||||
let current: unknown = state;
|
||||
for (const part of parts) {
|
||||
if (current == null || typeof current !== 'object') return 0;
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
return typeof current === 'number' ? current : 0;
|
||||
}
|
||||
|
||||
function getProgress(state: Record<string, unknown>, condition: AchievementCondition): { current: number; target: number; pct: number } | null {
|
||||
if (condition.field.startsWith('meta._')) return null;
|
||||
if (condition.operator === 'gt' && condition.value === 0) {
|
||||
const current = resolveField(state, condition.field);
|
||||
return { current, target: 1, pct: current > 0 ? 100 : 0 };
|
||||
}
|
||||
if (condition.operator === 'gte' || condition.operator === 'eq') {
|
||||
const current = resolveField(state, condition.field);
|
||||
const pct = condition.value > 0 ? Math.min(100, (current / condition.value) * 100) : (current > 0 ? 100 : 0);
|
||||
return { current, target: condition.value, pct };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function AchievementsPage() {
|
||||
const unlocked = useGameStore((s) => s.achievements.unlocked);
|
||||
const unlockedIds = new Set(unlocked.map(a => a.id));
|
||||
const state = useGameStore.getState() as unknown as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -29,6 +56,7 @@ export function AchievementsPage() {
|
||||
{ACHIEVEMENT_DEFINITIONS.map(def => {
|
||||
const isUnlocked = unlockedIds.has(def.id);
|
||||
const IconComponent = ICON_MAP[def.icon] ?? Trophy;
|
||||
const progress = !isUnlocked ? getProgress(state, def.condition) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -43,12 +71,23 @@ export function AchievementsPage() {
|
||||
<div className={`p-2 rounded-lg ${isUnlocked ? 'bg-accent/20 text-accent-light' : 'bg-surface-800 text-surface-500'}`}>
|
||||
{isUnlocked ? <IconComponent size={20} /> : <Lock size={20} />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className={`font-semibold text-sm ${isUnlocked ? '' : 'text-surface-400'}`}>{def.name}</h4>
|
||||
<p className="text-xs text-surface-400 mt-0.5">{def.description}</p>
|
||||
{isUnlocked && (
|
||||
<p className="text-xs text-accent mt-1">Unlocked</p>
|
||||
)}
|
||||
{!isUnlocked && progress && progress.target > 0 && (
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between text-[10px] text-surface-500 mb-0.5">
|
||||
<span>{formatNumber(progress.current)} / {formatNumber(progress.target)}</span>
|
||||
<span>{Math.floor(progress.pct)}%</span>
|
||||
</div>
|
||||
<div className="h-1 bg-surface-700 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-accent/50 rounded-full transition-all" style={{ width: `${progress.pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user