feat(ui): complete Task 4 UI redesign — all sub-tasks 1-10
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 8m47s

- Implemented complete design system with 40+ CSS custom properties
- Created 9 UI primitives (GameCard, SectionHeader, StatRow, ManaBar, ElementBadge, ValueDisplay, ActionButton, SkillRow, TooltipInfo)
- Redesigned all tabs: Spire, Skills, Stats, Equipment, Crafting, Attunements, Golemancy, Spells, Loot, Achievements, Lab, Debug
- Added toast notification system (GameToast) with success/warning/error/info types
- Added confirmation dialogs for destructive actions
- Removed all dev artifacts and component name labels
- Added empty states to all tabs
- Replaced emoji icons with Lucide React icons
- Added enchantPower placeholder to StatsTab and EquipmentTab
- Mobile audit passed at 375px viewport
- Build passes with 0 errors, lint passes with 0 errors

Sub-tasks completed:
- ST1: Design System Implementation
- ST2: Global Layout & Header
- ST3: Left Panel (Mana Display & Action Area)
- ST4: Skills Tab
- ST5: Spire Tab & Spire Mode UI
- ST6: Stats Tab
- ST7: Equipment & Crafting Tabs
- ST8: Attunements Tab
- ST9: Remaining Tabs
- ST10: Toast System & Confirmation Dialogs

Documentation: 15+ files in docs/task4/
This commit is contained in:
Refactoring Agent
2026-04-28 11:38:45 +02:00
parent 3c29c1c834
commit 47c71e6f54
61 changed files with 6892 additions and 1842 deletions
+142 -112
View File
@@ -1,16 +1,25 @@
'use client';
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { GameCard } from '@/components/ui/game-card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { ActionButton } from '@/components/ui/action-button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Progress } from '@/components/ui/progress';
import { ManaBar } from '@/components/ui/mana-bar';
import { Trophy, Lock, CheckCircle, ChevronDown, ChevronUp } from 'lucide-react';
import type { AchievementState } from '@/lib/game/types';
import { ACHIEVEMENTS, ACHIEVEMENT_CATEGORY_COLORS, getAchievementsByCategory, isAchievementRevealed } from '@/lib/game/data/achievements';
import { ACHIEVEMENTS, getAchievementsByCategory, isAchievementRevealed } from '@/lib/game/data/achievements';
import { GameState } from '@/lib/game/types';
// Map achievement categories to CSS variables for colors
const CATEGORY_COLOR_MAP: Record<string, string> = {
combat: 'var(--color-danger)',
progression: 'var(--rarity-legendary)',
crafting: 'var(--mana-dark)',
magic: 'var(--mana-water)',
special: 'var(--mana-stellar)',
};
interface AchievementsProps {
achievements: AchievementState;
gameState: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'totalSpellsCast' | 'totalDamageDealt' | 'totalCraftsCompleted'>;
@@ -55,120 +64,141 @@ export function AchievementsDisplay({ achievements, gameState }: AchievementsPro
};
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
<Trophy className="w-4 h-4" />
<GameCard variant="default" className="w-full">
<div className="flex items-center gap-2 mb-3">
<Trophy className="w-4 h-4 text-[var(--mana-light)]" />
<h3 className="text-[var(--mana-light)] game-panel-title text-xs uppercase tracking-wider">
Achievements
<Badge className="ml-auto bg-amber-900/50 text-amber-300">
{unlockedCount} / {totalCount}
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
<div className="space-y-2">
{Object.entries(categories).map(([category, categoryAchievements]) => (
<div key={category} className="space-y-1">
<Button
variant="ghost"
size="sm"
className="w-full justify-between text-xs"
onClick={() => setExpandedCategory(expandedCategory === category ? null : category)}
>
<span style={{ color: ACHIEVEMENT_CATEGORY_COLORS[category] }}>
{category.charAt(0).toUpperCase() + category.slice(1)}
</span>
<span className="text-gray-500">
{categoryAchievements.filter(a => achievements.unlocked.includes(a.id)).length} / {categoryAchievements.length}
</span>
{expandedCategory === category ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</Button>
{expandedCategory === category && (
<div className="pl-2 space-y-2">
{categoryAchievements.map((achievement) => {
const isUnlocked = achievements.unlocked.includes(achievement.id);
const progress = getProgress(achievement.id);
const isRevealed = isAchievementRevealed(achievement, progress);
const progressPercent = Math.min(100, (progress / achievement.requirement.value) * 100);
if (!isRevealed && !isUnlocked) {
return (
<div key={achievement.id} className="p-2 rounded bg-gray-800/30 border border-gray-700">
<div className="flex items-center gap-2 text-gray-500">
<Lock className="w-4 h-4" />
<span className="text-sm">???</span>
</div>
</div>
);
}
</h3>
<Badge
className="ml-auto bg-[var(--bg-sunken)] text-[var(--text-secondary)] border-[var(--border-subtle)]"
aria-label={`${unlockedCount} out of ${totalCount} achievements unlocked`}
>
{unlockedCount} / {totalCount}
</Badge>
</div>
<ScrollArea className="h-64 w-full">
<div className="space-y-2 pr-2">
{Object.entries(categories).map(([category, categoryAchievements]) => (
<div key={category} className="space-y-1">
<ActionButton
variant="ghost"
size="sm"
className="w-full justify-between text-xs hover:bg-[var(--bg-sunken)]"
onClick={() => setExpandedCategory(expandedCategory === category ? null : category)}
aria-expanded={expandedCategory === category}
aria-label={`${category} category - ${categoryAchievements.filter(a => achievements.unlocked.includes(a.id)).length} of ${categoryAchievements.length} unlocked`}
>
<span style={{ color: CATEGORY_COLOR_MAP[category] || 'var(--text-primary)' }}>
{category.charAt(0).toUpperCase() + category.slice(1)}
</span>
<span className="text-[var(--text-muted)]">
{categoryAchievements.filter(a => achievements.unlocked.includes(a.id)).length} / {categoryAchievements.length}
</span>
{expandedCategory === category ? (
<ChevronUp className="w-4 h-4 text-[var(--text-muted)]" />
) : (
<ChevronDown className="w-4 h-4 text-[var(--text-muted)]" />
)}
</ActionButton>
{expandedCategory === category && (
<div className="pl-2 space-y-2">
{categoryAchievements.map((achievement) => {
const isUnlocked = achievements.unlocked.includes(achievement.id);
const progress = getProgress(achievement.id);
const isRevealed = isAchievementRevealed(achievement, progress);
const progressPercent = Math.min(100, (progress / achievement.requirement.value) * 100);
if (!isRevealed && !isUnlocked) {
return (
<div
key={achievement.id}
className={`p-2 rounded border ${
isUnlocked
? 'bg-amber-900/20 border-amber-600/50'
: 'bg-gray-800/30 border-gray-700'
}`}
<div
key={achievement.id}
className="p-2 rounded bg-[var(--bg-sunken)] border border-[var(--border-subtle)]"
aria-label="Locked achievement - details hidden"
>
<div className="flex items-start justify-between mb-1">
<div className="flex items-center gap-2">
{isUnlocked ? (
<CheckCircle className="w-4 h-4 text-amber-400" />
) : (
<Trophy className="w-4 h-4 text-gray-500" />
)}
<span className={`text-sm font-semibold ${isUnlocked ? 'text-amber-300' : 'text-gray-300'}`}>
{achievement.name}
</span>
</div>
{achievement.reward.title && isUnlocked && (
<Badge className="text-xs bg-purple-900/50 text-purple-300">
Title
</Badge>
)}
<div className="flex items-center gap-2 text-[var(--text-muted)]">
<Lock className="w-4 h-4" aria-hidden="true" />
<span className="text-sm">???</span>
</div>
<div className="text-xs text-gray-400 mb-2">
{achievement.desc}
</div>
{!isUnlocked && (
<div className="space-y-1">
<Progress value={progressPercent} className="h-1 bg-gray-700" />
<div className="flex justify-between text-xs text-gray-500">
<span>{progress.toLocaleString()} / {achievement.requirement.value.toLocaleString()}</span>
<span>{progressPercent.toFixed(0)}%</span>
</div>
</div>
)}
{isUnlocked && achievement.reward && (
<div className="text-xs text-amber-400/70">
Reward:
{achievement.reward.insight && ` +${achievement.reward.insight} Insight`}
{achievement.reward.manaBonus && ` +${achievement.reward.manaBonus} Max Mana`}
{achievement.reward.damageBonus && ` +${(achievement.reward.damageBonus * 100).toFixed(0)}% Damage`}
{achievement.reward.title && ` "${achievement.reward.title}"`}
</div>
)}
</div>
);
})}
</div>
)}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
}
return (
<div
key={achievement.id}
className={`p-2 rounded border ${
isUnlocked
? 'bg-[var(--rarity-legendary-glow)] border-[var(--rarity-legendary)]/50'
: 'bg-[var(--bg-sunken)] border-[var(--border-subtle)]'
}`}
>
<div className="flex items-start justify-between mb-1">
<div className="flex items-center gap-2">
{isUnlocked ? (
<CheckCircle className="w-4 h-4 text-[var(--mana-light)]" aria-hidden="true" />
) : (
<Trophy className="w-4 h-4 text-[var(--text-muted)]" aria-hidden="true" />
)}
<span
className={`text-sm font-semibold ${
isUnlocked ? 'text-[var(--mana-light)]' : 'text-[var(--text-secondary)]'
}`}
>
{achievement.name}
</span>
</div>
{achievement.reward.title && isUnlocked && (
<Badge
className="text-xs bg-[var(--mana-dark)]/20 text-[var(--mana-dark)] border-[var(--mana-dark)]/40"
aria-label="Title reward"
>
Title
</Badge>
)}
</div>
<div className="text-xs text-[var(--text-muted)] mb-2">
{achievement.desc}
</div>
{!isUnlocked && (
<div className="space-y-1">
<ManaBar
value={progress}
max={achievement.requirement.value}
manaType="light"
className="h-1.5"
aria-label={`Progress: ${Math.round(progressPercent)}%`}
/>
<div className="flex justify-between text-xs text-[var(--text-muted)]">
<span>{progress.toLocaleString()} / {achievement.requirement.value.toLocaleString()}</span>
<span>{progressPercent.toFixed(0)}%</span>
</div>
</div>
)}
{isUnlocked && achievement.reward && (
<div className="text-xs text-[var(--mana-light)]/70">
Reward:
{achievement.reward.insight && ` +${achievement.reward.insight} Insight`}
{achievement.reward.manaBonus && ` +${achievement.reward.manaBonus} Max Mana`}
{achievement.reward.damageBonus && ` +${(achievement.reward.damageBonus * 100).toFixed(0)}% Damage`}
{achievement.reward.title && ` "${achievement.reward.title}"`}
</div>
)}
</div>
);
})}
</div>
)}
</div>
))}
</div>
</ScrollArea>
</GameCard>
);
}