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:
@@ -0,0 +1,167 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import {
|
||||
Mountain,
|
||||
Sparkles,
|
||||
Brain,
|
||||
Wand2,
|
||||
Bone,
|
||||
Shield,
|
||||
Hammer,
|
||||
Gem,
|
||||
Trophy,
|
||||
FlaskConical,
|
||||
BarChart3,
|
||||
BookOpen,
|
||||
Wrench
|
||||
} from 'lucide-react';
|
||||
|
||||
interface TabBarProps {
|
||||
activeTab: string;
|
||||
onTabChange: (value: string) => void;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
// Tab configuration with groups
|
||||
const TAB_GROUPS = [
|
||||
{
|
||||
name: 'World',
|
||||
tabs: [
|
||||
{ value: 'spire', label: 'Spire', icon: Mountain, mobileLabel: 'Spire' },
|
||||
{ value: 'attunements', label: 'Attune', icon: Sparkles, mobileLabel: 'Attune' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Power',
|
||||
tabs: [
|
||||
{ value: 'skills', label: 'Skills', icon: Brain, mobileLabel: 'Skills' },
|
||||
{ value: 'spells', label: 'Spells', icon: Wand2, mobileLabel: 'Spells' },
|
||||
{ value: 'golemancy', label: 'Golems', icon: Bone, mobileLabel: 'Golems' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Gear',
|
||||
tabs: [
|
||||
{ value: 'equipment', label: 'Gear', icon: Shield, mobileLabel: 'Gear' },
|
||||
{ value: 'crafting', label: 'Craft', icon: Hammer, mobileLabel: 'Craft' },
|
||||
{ value: 'loot', label: 'Loot', icon: Gem, mobileLabel: 'Loot' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Meta',
|
||||
tabs: [
|
||||
{ value: 'achievements', label: 'Achieve', icon: Trophy, mobileLabel: 'Achieve' },
|
||||
{ value: 'lab', label: 'Lab', icon: FlaskConical, mobileLabel: 'Lab' },
|
||||
{ value: 'stats', label: 'Stats', icon: BarChart3, mobileLabel: 'Stats' },
|
||||
{ value: 'grimoire', label: 'Grimoire', icon: BookOpen, mobileLabel: 'Grimoire' },
|
||||
{ value: 'debug', label: 'Debug', icon: Wrench, mobileLabel: 'Debug' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export function TabBar({ activeTab, onTabChange, isMobile = false }: TabBarProps) {
|
||||
const [longPressTimer, setLongPressTimer] = useState<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleLongPressStart = (value: string) => {
|
||||
const timer = setTimeout(() => {
|
||||
// Show tooltip on long press for mobile
|
||||
onTabChange(value);
|
||||
}, 500);
|
||||
setLongPressTimer(timer);
|
||||
};
|
||||
|
||||
const handleLongPressEnd = () => {
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer);
|
||||
setLongPressTimer(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex overflow-x-auto scrollbar-thin gap-1 pb-2" style={{ flexWrap: 'nowrap' }}>
|
||||
{TAB_GROUPS.map((group, groupIndex) => (
|
||||
<div key={group.name} className="flex items-center flex-shrink-0">
|
||||
{groupIndex > 0 && (
|
||||
<Separator orientation="vertical" className="h-6 mx-1 bg-[var(--border-subtle)]" />
|
||||
)}
|
||||
{group.tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.value;
|
||||
return (
|
||||
<Tooltip key={tab.value}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => onTabChange(tab.value)}
|
||||
onMouseDown={() => handleLongPressStart(tab.value)}
|
||||
onMouseUp={handleLongPressEnd}
|
||||
onMouseLeave={handleLongPressEnd}
|
||||
onTouchStart={() => handleLongPressStart(tab.value)}
|
||||
onTouchEnd={handleLongPressEnd}
|
||||
className={`
|
||||
flex items-center justify-center p-2 rounded-lg transition-all flex-shrink-0
|
||||
${isActive
|
||||
? 'bg-[var(--interactive-primary)] text-white shadow-lg shadow-[var(--interactive-primary)]/20'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-elevated)]'
|
||||
}
|
||||
`}
|
||||
aria-label={tab.label}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<p>{tab.label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop view - grouped tabs with separators
|
||||
return (
|
||||
<div className="flex items-center gap-1 w-full" style={{ flexWrap: 'nowrap' }}>
|
||||
{TAB_GROUPS.map((group, groupIndex) => (
|
||||
<div key={group.name} className="flex items-center flex-shrink-0">
|
||||
{groupIndex > 0 && (
|
||||
<Separator orientation="vertical" className="h-6 mx-2 bg-[var(--border-subtle)]" />
|
||||
)}
|
||||
{group.tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.value;
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className={`
|
||||
text-xs px-3 py-1.5 relative transition-all whitespace-nowrap
|
||||
${isActive
|
||||
? 'text-[var(--interactive-primary)] font-semibold'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
style={isActive ? {
|
||||
borderBottom: '2px solid var(--interactive-primary)',
|
||||
textShadow: '0 0 8px var(--interactive-primary)',
|
||||
} : {}}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TabBar.displayName = "TabBar";
|
||||
Reference in New Issue
Block a user