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,141 @@
|
||||
'use client';
|
||||
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from '@/components/ui/toast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
// Toast type definitions
|
||||
type ToastType = 'success' | 'warning' | 'error' | 'info';
|
||||
|
||||
interface ToastIconProps {
|
||||
type: ToastType;
|
||||
}
|
||||
|
||||
// Icon mapping for toast types
|
||||
function ToastIcon({ type }: ToastIconProps) {
|
||||
const iconClass = 'h-4 w-4 shrink-0';
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return <CheckCircle className={cn(iconClass, 'text-[var(--color-success)]')} />;
|
||||
case 'warning':
|
||||
return <AlertTriangle className={cn(iconClass, 'text-[var(--color-warning)]')} />;
|
||||
case 'error':
|
||||
return <AlertCircle className={cn(iconClass, 'text-[var(--color-danger)]')} />;
|
||||
case 'info':
|
||||
return <Info className={cn(iconClass, 'text-[var(--color-info)]')} />;
|
||||
}
|
||||
}
|
||||
|
||||
// Color mapping for toast types using design system tokens
|
||||
const TOAST_TYPE_STYLES: Record<ToastType, string> = {
|
||||
success: 'border-[var(--color-success)]/50 bg-[var(--color-success)]/10',
|
||||
warning: 'border-[var(--color-warning)]/50 bg-[var(--color-warning)]/10',
|
||||
error: 'border-[var(--color-danger)]/50 bg-[var(--color-danger)]/10',
|
||||
info: 'border-[var(--color-info)]/50 bg-[var(--color-info)]/10',
|
||||
};
|
||||
|
||||
const TOAST_TYPE_TEXT: Record<ToastType, string> = {
|
||||
success: 'text-[var(--color-success)]',
|
||||
warning: 'text-[var(--color-warning)]',
|
||||
error: 'text-[var(--color-danger)]',
|
||||
info: 'text-[var(--color-info)]',
|
||||
};
|
||||
|
||||
export function GameToaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map((toast) => {
|
||||
// Determine toast type from className or default to info
|
||||
const toastType: ToastType =
|
||||
toast.variant === 'destructive' ? 'error' :
|
||||
(toast as { toastType?: ToastType }).toastType || 'info';
|
||||
|
||||
return (
|
||||
<Toast
|
||||
key={toast.id}
|
||||
className={cn(
|
||||
'group pointer-events-auto relative flex w-full items-center justify-between space-x-3 overflow-hidden rounded-md border p-4 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
||||
TOAST_TYPE_STYLES[toastType]
|
||||
)}
|
||||
{...toast}
|
||||
>
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<ToastIcon type={toastType} />
|
||||
<div className="grid gap-1 flex-1">
|
||||
{toast.title && (
|
||||
<ToastTitle className={cn('text-sm font-semibold', TOAST_TYPE_TEXT[toastType])}>
|
||||
{toast.title}
|
||||
</ToastTitle>
|
||||
)}
|
||||
{toast.description && (
|
||||
<ToastDescription className="text-xs text-[var(--text-secondary)]">
|
||||
{toast.description}
|
||||
</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ToastClose className="absolute right-1 top-1 rounded-md p-1 text-[var(--text-muted)] opacity-0 transition-opacity hover:text-[var(--text-primary)] focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-70">
|
||||
<X className="h-3 w-3" />
|
||||
</ToastClose>
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
{/*
|
||||
Viewport positioning:
|
||||
- Desktop: bottom-right
|
||||
- Mobile: bottom-center, full-width
|
||||
*/}
|
||||
<ToastViewport
|
||||
className={cn(
|
||||
'fixed z-[100] flex max-h-screen w-full flex-col-reverse p-4',
|
||||
// Desktop: bottom-right, fixed width
|
||||
'sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col sm:max-w-[420px]',
|
||||
// Mobile: bottom-center, full-width
|
||||
'max-sm:bottom-0 max-sm:left-0 max-sm:flex-col max-sm:items-center'
|
||||
)}
|
||||
/>
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom hook to show typed toasts
|
||||
export function useGameToast() {
|
||||
const { toast } = useToast();
|
||||
|
||||
return (type: ToastType, title: ReactNode, description?: ReactNode) => {
|
||||
const toastTypeClass = `toast-type-${type}`;
|
||||
|
||||
return toast({
|
||||
title,
|
||||
description,
|
||||
className: toastTypeClass,
|
||||
// Store the type for styling
|
||||
...{ toastType: type },
|
||||
} as {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
className?: string;
|
||||
toastType?: ToastType;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export { type ToastType };
|
||||
Reference in New Issue
Block a user