Files
Mana-Loop/src/components/game/GameToast.tsx
T
Refactoring Agent 47c71e6f54
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 8m47s
feat(ui): complete Task 4 UI redesign — all sub-tasks 1-10
- 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/
2026-04-28 11:38:45 +02:00

142 lines
4.8 KiB
TypeScript

'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 };