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
+94 -46
View File
@@ -1,7 +1,6 @@
'use client';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { GameCard, ElementBadge } from '@/components/ui';
import { Badge } from '@/components/ui/badge';
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
@@ -53,12 +52,16 @@ export function SpellsTab({ store }: SpellsTabProps) {
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
};
const hasPactSpells = store.signedPacts.length > 0;
return (
<div className="space-y-6">
{/* Equipment-Granted Spells */}
<div className="mb-6">
<h3 className="text-lg font-semibold mb-3 text-cyan-400"> Known Spells</h3>
<p className="text-sm text-gray-400 mb-4">
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--mana-crystal)]">
Known Spells
</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">
Spells are obtained by enchanting equipment with spell effects.
Visit the Crafting tab to design and apply enchantments.
</p>
@@ -75,61 +78,90 @@ export function SpellsTab({ store }: SpellsTabProps) {
const sources = spellSources[id] || [];
return (
<Card
<GameCard
key={id}
className={`bg-gray-900/80 border-cyan-600/50 ${canCast ? 'ring-1 ring-green-500/30' : ''}`}
className={canCast ? 'ring-1 ring-[var(--color-success)]/30' : ''}
>
<CardHeader className="pb-2">
<div className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
<h4
className="text-sm font-semibold"
style={{ color: def.elem === 'raw' ? 'var(--mana-transfer)' : `var(--mana-${def.elem})` }}
>
{def.name}
</CardTitle>
<Badge className="bg-cyan-900/50 text-cyan-300 text-xs">Equipment</Badge>
</h4>
<Badge className="bg-[var(--bg-elevated)] text-[var(--mana-crystal)] text-xs border border-[var(--mana-crystal)]/30">
Equipment
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-2">
<div className="text-xs text-gray-400">
{def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>}
</div>
<div className="space-y-2">
<div className="text-xs text-[var(--text-secondary)]">
{def.elem !== 'raw' && (
<span className="mr-2">
<ElementBadge elementId={def.elem} size="sm" /> {elemDef?.name}
</span>
)}
<span> {def.dmg} dmg</span>
</div>
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
<div className="text-xs font-[var(--font-mono)]" style={{ color: getSpellCostColor(def.cost) }}>
Cost: {formatSpellCost(def.cost)}
</div>
<div className="text-xs text-cyan-400/70">From: {sources.join(', ')}</div>
<div className="text-xs text-[var(--mana-crystal)]/70">From: {sources.join(', ')}</div>
<div className="flex gap-2">
{isActive ? (
<Badge className="bg-amber-900/50 text-amber-300">Active</Badge>
<Badge className="bg-[var(--bg-elevated)] text-[var(--color-warning)] border border-[var(--color-warning)]/30">
Active
</Badge>
) : (
<Button size="sm" variant="outline" onClick={() => store.setSpell(id)}>
<button
className="px-3 py-1 text-xs border border-[var(--border-default)] rounded hover:border-[var(--border-focus)] transition-colors"
onClick={() => store.setSpell(id)}
>
Set Active
</Button>
</button>
)}
</div>
</CardContent>
</Card>
</div>
</GameCard>
);
})}
</div>
) : (
<div className="text-center p-8 bg-gray-800/30 rounded border border-gray-700">
<div className="text-gray-500 mb-2">No spells known yet</div>
<div className="text-sm text-gray-600">Enchant a staff with a spell effect to gain spells</div>
<div className="text-center p-8 bg-[var(--bg-sunken)] rounded border border-[var(--border-subtle)]">
<div className="text-[var(--text-muted)] mb-2">No spells known yet</div>
<div className="text-sm text-[var(--text-muted)]">Enchant a staff with a spell effect to gain spells</div>
</div>
)}
</div>
{/* Pact Spells (from guardian defeats) */}
{store.signedPacts.length > 0 && (
{/* Pact Spells (from guardian defeats) - Empty State */}
{!hasPactSpells && (
<div className="mb-6">
<h3 className="text-lg font-semibold mb-3 text-amber-400">🏆 Pact Spells</h3>
<p className="text-sm text-gray-400 mb-3">Spells earned through guardian pacts appear here.</p>
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--color-warning)]">
Pact Spells
</h3>
<div className="text-center p-6 bg-[var(--bg-sunken)] rounded border border-[var(--border-subtle)]">
<p className="text-sm text-[var(--text-muted)]">Defeat guardians and sign pacts to unlock powerful spells</p>
</div>
</div>
)}
{hasPactSpells && (
<div className="mb-6">
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--color-warning)]">
Pact Spells
</h3>
<p className="text-sm text-[var(--text-secondary)] mb-3">Spells earned through guardian pacts appear here.</p>
</div>
)}
{/* Spell Reference - show all available spells for enchanting */}
<div className="mb-6">
<h3 className="text-lg font-semibold mb-3 text-purple-400">📚 Spell Reference</h3>
<p className="text-sm text-gray-400 mb-4">
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--mana-death)]">
Spell Reference
</h3>
<p className="text-sm text-[var(--text-secondary)] mb-4">
These spells can be applied to equipment through the enchanting system.
Research enchantment effects in the Skills tab to unlock them for designing.
</p>
@@ -140,37 +172,53 @@ export function SpellsTab({ store }: SpellsTabProps) {
const isUnlocked = store.unlockedEffects?.includes(`spell_${id}`);
return (
<Card
<GameCard
key={id}
className={`bg-gray-900/80 border-gray-700 ${isUnlocked ? 'border-purple-500/50' : 'opacity-60'}`}
variant={isUnlocked ? "default" : "sunken"}
className={isUnlocked ? 'border-[var(--mana-death)]/50' : 'opacity-60'}
>
<CardHeader className="pb-2">
<div className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
<h4
className="text-sm font-semibold"
style={{ color: def.elem === 'raw' ? 'var(--mana-transfer)' : `var(--mana-${def.elem})` }}
>
{def.name}
</CardTitle>
</h4>
<div className="flex gap-1">
{def.tier > 0 && <Badge variant="outline" className="text-xs">T{def.tier}</Badge>}
{isUnlocked && <Badge className="bg-purple-900/50 text-purple-300 text-xs">Unlocked</Badge>}
{def.tier > 0 && (
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
T{def.tier}
</span>
)}
{isUnlocked && (
<Badge className="bg-[var(--bg-elevated)] text-[var(--mana-death)] text-xs border border-[var(--mana-death)]/30">
Unlocked
</Badge>
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-2">
<div className="text-xs text-gray-400">
{def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>}
</div>
<div className="space-y-2">
<div className="text-xs text-[var(--text-secondary)]">
{def.elem !== 'raw' && (
<span className="mr-2">
<ElementBadge elementId={def.elem} size="sm" /> {elemDef?.name}
</span>
)}
<span> {def.dmg} dmg</span>
</div>
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
<div className="text-xs font-[var(--font-mono)]" style={{ color: getSpellCostColor(def.cost) }}>
Cost: {formatSpellCost(def.cost)}
</div>
{def.desc && (
<div className="text-xs text-gray-500 italic">{def.desc}</div>
<div className="text-xs text-[var(--text-muted)] italic">{def.desc}</div>
)}
{!isUnlocked && (
<div className="text-xs text-amber-400/70">Research to unlock for enchanting</div>
<div className="text-xs text-[var(--color-warning)]/70">Research to unlock for enchanting</div>
)}
</CardContent>
</Card>
</div>
</GameCard>
);
})}
</div>