47c71e6f54
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/
142 lines
4.8 KiB
TypeScript
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 };
|