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
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:
@@ -1,11 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { GameCard, StatRow, ElementBadge, ActionButton } from '@/components/ui';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Mountain, Zap, Clock, Swords, Target, Sparkles, Lock, Check, X
|
||||
Mountain, Zap, Clock, Swords, Target, Sparkles, Lock, Check, X,
|
||||
Info, HelpCircle
|
||||
} from 'lucide-react';
|
||||
import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration } from '@/lib/game/data/golems';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
@@ -65,19 +65,19 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
|
||||
// Get element color
|
||||
const primaryElement = getElementInfo(golem.baseManaType);
|
||||
const elementColor = primaryElement?.color || '#888';
|
||||
const elementId = golem.baseManaType;
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Locked golem card
|
||||
return (
|
||||
<Card key={golemId} className="bg-gray-900/80 border-gray-700 opacity-50">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<GameCard key={golemId} variant="sunken" className="opacity-60">
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
||||
<Lock className="w-4 h-4" />
|
||||
<span className="text-gray-500">???</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-xs text-gray-500">
|
||||
<span className="text-[var(--text-muted)]">???</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">
|
||||
{golem.unlockCondition.type === 'attunement_level' && (
|
||||
<div>Requires Fabricator Level {golem.unlockCondition.level}</div>
|
||||
)}
|
||||
@@ -87,73 +87,65 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
{golem.unlockCondition.type === 'dual_attunement' && (
|
||||
<div>Requires Enchanter & Fabricator Level 5</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</GameCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
<GameCard
|
||||
key={golemId}
|
||||
className={`bg-gray-900/80 border-2 transition-all cursor-pointer ${
|
||||
variant={isEnabled ? "default" : "sunken"}
|
||||
className={`transition-all cursor-pointer border-2 ${
|
||||
isEnabled
|
||||
? 'border-green-500 bg-green-900/10'
|
||||
: 'border-gray-700 hover:border-gray-600'
|
||||
? 'border-[var(--color-success)] bg-[var(--bg-surface)]'
|
||||
: 'border-[var(--border-subtle)] hover:border-[var(--border-default)]'
|
||||
}`}
|
||||
onClick={() => toggleGolem(golemId)}
|
||||
aria-label={`${isEnabled ? 'Disable' : 'Enable'} ${golem.name}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm flex items-center justify-between">
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mountain className="w-4 h-4" style={{ color: elementColor }} />
|
||||
<span style={{ color: elementColor }}>{golem.name}</span>
|
||||
<Mountain className="w-4 h-4" style={{ color: `var(--mana-${elementId})` }} />
|
||||
<span style={{ color: `var(--mana-${elementId})` }}>{golem.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{golem.isAoe && (
|
||||
<Badge variant="outline" className="text-xs">AOE {golem.aoeTargets}</Badge>
|
||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
||||
AOE {golem.aoeTargets}
|
||||
</span>
|
||||
)}
|
||||
<Badge variant="outline" className="text-xs">T{golem.tier}</Badge>
|
||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
||||
T{golem.tier}
|
||||
</span>
|
||||
{isEnabled ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
<Check className="w-4 h-4 text-[var(--color-success)]" />
|
||||
) : (
|
||||
<X className="w-4 h-4 text-gray-500" />
|
||||
<X className="w-4 h-4 text-[var(--text-muted)]" />
|
||||
)}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-xs text-gray-400">{golem.description}</p>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-[var(--text-secondary)]">{golem.description}</p>
|
||||
|
||||
<Separator className="bg-gray-700" />
|
||||
<Separator className="bg-[var(--border-subtle)]" />
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Swords className="w-3 h-3 text-red-400" />
|
||||
<span className="text-gray-400">DMG:</span>
|
||||
<span className="text-white">{damage}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3 text-yellow-400" />
|
||||
<span className="text-gray-400">Speed:</span>
|
||||
<span className="text-white">{attackSpeed.toFixed(1)}/hr</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Target className="w-3 h-3 text-blue-400" />
|
||||
<span className="text-gray-400">Pierce:</span>
|
||||
<span className="text-white">{Math.floor(golem.armorPierce * 100)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3 text-purple-400" />
|
||||
<span className="text-gray-400">Duration:</span>
|
||||
<span className="text-white">{floorDuration} floor(s)</span>
|
||||
</div>
|
||||
<StatRow label="DMG:" value={damage.toString()} />
|
||||
<StatRow label="Speed:" value={`${attackSpeed.toFixed(1)}/hr`} />
|
||||
<StatRow label="Pierce:" value={`${Math.floor(golem.armorPierce * 100)}%`} />
|
||||
<StatRow label="Duration:" value={`${floorDuration} floor(s)`} />
|
||||
</div>
|
||||
|
||||
<Separator className="bg-gray-700" />
|
||||
<Separator className="bg-[var(--border-subtle)]" />
|
||||
|
||||
{/* Summon Cost */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">Summon Cost:</div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">Summon Cost:</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{golem.summonCost.map((cost, idx) => {
|
||||
const elem = getElementInfo(cost.element || '');
|
||||
@@ -163,15 +155,17 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
const canAfford = available >= cost.amount;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
<span
|
||||
key={idx}
|
||||
variant="outline"
|
||||
className={`text-xs ${canAfford ? 'border-green-500' : 'border-red-500'}`}
|
||||
style={{ borderColor: canAfford ? undefined : '#ef4444' }}
|
||||
className={`text-xs px-1.5 py-0.5 border rounded ${
|
||||
canAfford
|
||||
? 'border-[var(--color-success)] text-[var(--color-success)]'
|
||||
: 'border-[var(--color-danger)] text-[var(--color-danger)]'
|
||||
}`}
|
||||
>
|
||||
<span style={{ color: elem?.color }}>{elem?.sym || '💎'}</span>
|
||||
{cost.element && <ElementBadge elementId={cost.element} size="sm" />}
|
||||
{' '}{cost.amount}
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -179,16 +173,14 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
|
||||
{/* Maintenance Cost */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mb-1">Maintenance/hr:</div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">Maintenance/hr:</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{golem.maintenanceCost.map((cost, idx) => {
|
||||
const elem = getElementInfo(cost.element || '');
|
||||
|
||||
return (
|
||||
<Badge key={idx} variant="outline" className="text-xs">
|
||||
<span style={{ color: elem?.color }}>{elem?.sym || '💎'}</span>
|
||||
<span key={idx} className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
||||
{cost.element && <ElementBadge elementId={cost.element} size="sm" />}
|
||||
{' '}{cost.amount}/hr
|
||||
</Badge>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -196,143 +188,131 @@ export function GolemancyTab({ store }: GolemancyTabProps) {
|
||||
|
||||
{/* Status */}
|
||||
{isSelected && (
|
||||
<div className="mt-2 text-xs text-green-400 flex items-center gap-1">
|
||||
<div className="mt-2 text-xs text-[var(--color-success)] flex items-center gap-1">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
Active on Floor {currentFloor}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</GameCard>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Mountain className="w-5 h-5 text-amber-500" />
|
||||
<GameCard>
|
||||
<div className="pb-2">
|
||||
<h2 className="text-lg font-[var(--font-heading)] font-semibold flex items-center gap-2 text-[var(--text-primary)]">
|
||||
<Mountain className="w-5 h-5 text-[var(--mana-earth)]" />
|
||||
Golemancy
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{!hasGolemancy ? (
|
||||
<div className="text-center text-gray-400 py-4">
|
||||
<div className="text-center text-[var(--text-secondary)] py-4">
|
||||
<Lock className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>Unlock the Fabricator attunement and reach Level 2 to summon golems.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-400">Golem Slots:</span>
|
||||
<span className="text-sm font-semibold">
|
||||
<span className="text-amber-400">{golemancy.enabledGolems.length}</span>
|
||||
<span className="text-gray-500"> / {maxSlots}</span>
|
||||
</span>
|
||||
</div>
|
||||
<>
|
||||
<StatRow
|
||||
label="Golem Slots:"
|
||||
value={`${golemancy.enabledGolems.length} / ${maxSlots}`}
|
||||
highlight={golemancy.enabledGolems.length > 0 ? 'success' : undefined}
|
||||
/>
|
||||
<StatRow
|
||||
label="Fabricator Level:"
|
||||
value={fabricatorLevel.toString()}
|
||||
highlight="warning"
|
||||
/>
|
||||
<StatRow
|
||||
label="Floor Duration:"
|
||||
value={`${getGolemFloorDuration(skills)} floor(s)`}
|
||||
/>
|
||||
<StatRow
|
||||
label="Status:"
|
||||
value={inCombat ? 'Combat Active' : 'Puzzle Room (No Golems)'}
|
||||
highlight={inCombat ? 'success' : 'warning'}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-400">Fabricator Level:</span>
|
||||
<span className="text-sm font-semibold text-amber-400">{fabricatorLevel}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-400">Floor Duration:</span>
|
||||
<span className="text-sm font-semibold">{getGolemFloorDuration(skills)} floor(s)</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-400">Status:</span>
|
||||
<span className={`text-sm ${inCombat ? 'text-green-400' : 'text-yellow-400'}`}>
|
||||
{inCombat ? '⚔️ Combat Active' : '🧩 Puzzle Room (No Golems)'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
<p className="text-xs text-[var(--text-muted)] mt-2">
|
||||
Golems are automatically summoned at the start of each combat floor.
|
||||
They cost mana to maintain and will be dismissed if you run out.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
{/* Active Golems - Empty State */}
|
||||
{hasGolemancy && golemancy.summonedGolems.length === 0 && (
|
||||
<GameCard variant="sunken">
|
||||
<div className="text-center py-4 text-[var(--text-muted)]">
|
||||
<Info className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No golems summoned</p>
|
||||
<p className="text-xs mt-1">Enable golems below to summon them at the start of combat</p>
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Active Golems */}
|
||||
{hasGolemancy && golemancy.summonedGolems.length > 0 && (
|
||||
<Card className="bg-gray-900/80 border-green-600">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-green-400 flex items-center gap-2">
|
||||
<GameCard variant="default" className="border-[var(--color-success)]">
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--color-success)] flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Active Golems ({golemancy.summonedGolems.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{golemancy.summonedGolems.map(sg => {
|
||||
const golem = GOLEMS_DEF[sg.golemId];
|
||||
const elem = getElementInfo(golem?.baseManaType || '');
|
||||
|
||||
return (
|
||||
<Badge key={sg.golemId} variant="outline" className="text-sm py-1 px-2">
|
||||
<Mountain className="w-3 h-3 mr-1" style={{ color: elem?.color }} />
|
||||
{golem?.name}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{golemancy.summonedGolems.map(sg => {
|
||||
const golem = GOLEMS_DEF[sg.golemId];
|
||||
if (!golem) return null;
|
||||
|
||||
return (
|
||||
<span key={sg.golemId} className="text-xs px-2 py-1 border border-[var(--border-default)] rounded">
|
||||
<Mountain className="w-3 h-3 inline mr-1" style={{ color: `var(--mana-${golem.baseManaType})` }} />
|
||||
{golem.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Golem Selection */}
|
||||
{hasGolemancy && (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Select Golems to Summon</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-96">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pr-4">
|
||||
{/* Unlocked Golems */}
|
||||
{unlockedGolems.map(golem => renderGolemCard(golem.id, true))}
|
||||
|
||||
{/* Locked Golems */}
|
||||
{Object.values(GOLEMS_DEF)
|
||||
.filter(g => !isGolemUnlocked(g.id, attunements, unlockedElements))
|
||||
.map(golem => renderGolemCard(golem.id, false))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<GameCard>
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Select Golems to Summon</h3>
|
||||
</div>
|
||||
<ScrollArea className="h-96">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pr-4">
|
||||
{/* Unlocked Golems */}
|
||||
{unlockedGolems.map(golem => renderGolemCard(golem.id, true))}
|
||||
|
||||
{/* Locked Golems */}
|
||||
{Object.values(GOLEMS_DEF)
|
||||
.filter(g => !isGolemUnlocked(g.id, attunements, unlockedElements))
|
||||
.map(golem => renderGolemCard(golem.id, false))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Golemancy Skills Info */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm">Golemancy Skills</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-xs text-gray-400 space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span>Golem Mastery:</span>
|
||||
<span className="text-white">+{skills.golemMastery || 0}0% damage</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Golem Efficiency:</span>
|
||||
<span className="text-white">+{(skills.golemEfficiency || 0) * 5}% attack speed</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Golem Longevity:</span>
|
||||
<span className="text-white">+{skills.golemLongevity || 0} floor duration</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Golem Siphon:</span>
|
||||
<span className="text-white">-{(skills.golemSiphon || 0) * 10}% maintenance</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<GameCard>
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Golemancy Skills</h3>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<StatRow label="Golem Mastery:" value={`+${(skills.golemMastery || 0) * 10}% damage`} />
|
||||
<StatRow label="Golem Efficiency:" value={`+${(skills.golemEfficiency || 0) * 5}% attack speed`} />
|
||||
<StatRow label="Golem Longevity:" value={`+${skills.golemLongevity || 0} floor duration`} />
|
||||
<StatRow label="Golem Siphon:" value={`-${(skills.golemSiphon || 0) * 10}% maintenance`} />
|
||||
</div>
|
||||
</GameCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user