refactor: migrate frontend from shadcn/Tailwind to Mantine v9
Reescribe todos los componentes UI para usar Mantine v9 en lugar de shadcn/Tailwind. Elimina cn(), CVA, components.json, theme_provider custom y globals.css con Tailwind. Añade 25+ componentes nuevos (AppShell, AuthForm, DatePickerInput, Dropzone, etc.) y MantineProvider como wrapper estándar del sistema de temas. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+66
-139
@@ -1,112 +1,58 @@
|
||||
"use client"
|
||||
import { notifications } from '@mantine/notifications'
|
||||
import { Paper, Text, Group, CloseButton } from '@mantine/core'
|
||||
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { XIcon, CheckCircle2Icon, AlertCircleIcon, AlertTriangleIcon, InfoIcon } from "lucide-react"
|
||||
import { cn } from "../core/cn"
|
||||
type ToastVariant = 'default' | 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
const toastVariants = cva(
|
||||
"group/toast pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-xl border p-4 pr-8 shadow-lg transition-all",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground border-border",
|
||||
success: "bg-background border-l-4 border-l-green-500 border-border text-foreground",
|
||||
error: "bg-background border-l-4 border-l-destructive border-border text-foreground",
|
||||
warning: "bg-background border-l-4 border-l-yellow-500 border-border text-foreground",
|
||||
info: "bg-background border-l-4 border-l-blue-500 border-border text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: { variant: "default" },
|
||||
}
|
||||
)
|
||||
|
||||
const variantIcons: Record<string, React.ReactNode> = {
|
||||
success: <CheckCircle2Icon className="mt-0.5 size-4 shrink-0 text-green-500" />,
|
||||
error: <AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />,
|
||||
warning: <AlertTriangleIcon className="mt-0.5 size-4 shrink-0 text-yellow-500" />,
|
||||
info: <InfoIcon className="mt-0.5 size-4 shrink-0 text-blue-500" />,
|
||||
}
|
||||
|
||||
interface ToastProps
|
||||
extends React.ComponentPropsWithoutRef<"div">,
|
||||
VariantProps<typeof toastVariants> {
|
||||
interface ToastProps {
|
||||
variant?: ToastVariant
|
||||
title?: string
|
||||
description?: string
|
||||
action?: React.ReactNode
|
||||
onClose?: () => void
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
|
||||
({ className, variant = "default", title, description, action, onClose, children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="toast"
|
||||
data-variant={variant}
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{variant && variant !== "default" && variantIcons[variant]}
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
{title && (
|
||||
<div data-slot="toast-title" className="text-sm font-semibold leading-none">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{description && (
|
||||
<div data-slot="toast-description" className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{action && <div data-slot="toast-action" className="mt-2">{action}</div>}
|
||||
</div>
|
||||
{onClose && (
|
||||
<button
|
||||
data-slot="toast-close"
|
||||
onClick={onClose}
|
||||
className="absolute top-2 right-2 inline-flex size-5 items-center justify-center rounded-md text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-label="Close"
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
Toast.displayName = "Toast"
|
||||
|
||||
interface ToastViewportProps extends React.ComponentPropsWithoutRef<"div"> {
|
||||
position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"
|
||||
const variantColors: Record<ToastVariant, string | undefined> = {
|
||||
default: undefined,
|
||||
success: 'teal',
|
||||
error: 'red',
|
||||
warning: 'yellow',
|
||||
info: 'blue',
|
||||
}
|
||||
|
||||
const positionClasses: Record<NonNullable<ToastViewportProps["position"]>, string> = {
|
||||
"top-left": "top-4 left-4",
|
||||
"top-center": "top-4 left-1/2 -translate-x-1/2",
|
||||
"top-right": "top-4 right-4",
|
||||
"bottom-left": "bottom-4 left-4",
|
||||
"bottom-center": "bottom-4 left-1/2 -translate-x-1/2",
|
||||
"bottom-right": "bottom-4 right-4",
|
||||
}
|
||||
|
||||
function ToastViewport({ className, position = "bottom-right", ...props }: ToastViewportProps) {
|
||||
function Toast({ title, description, onClose, className }: ToastProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="toast-viewport"
|
||||
className={cn(
|
||||
"fixed z-[100] flex max-h-screen w-full max-w-sm flex-col gap-2 p-4",
|
||||
positionClasses[position],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<Paper withBorder shadow="md" p="md" radius="md" className={className}>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
{title && <Text size="sm" fw={600}>{title}</Text>}
|
||||
{description && <Text size="sm" c="dimmed">{description}</Text>}
|
||||
</div>
|
||||
{onClose && <CloseButton size="sm" onClick={onClose} />}
|
||||
</Group>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
const toastVariants = {
|
||||
default: 'default',
|
||||
success: 'success',
|
||||
error: 'error',
|
||||
warning: 'warning',
|
||||
info: 'info',
|
||||
} as const
|
||||
|
||||
interface ToastViewportProps {
|
||||
position?: 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
function ToastViewport({ children }: ToastViewportProps) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
type ToastEntry = ToastProps & {
|
||||
id: string
|
||||
duration?: number
|
||||
@@ -114,56 +60,37 @@ type ToastEntry = ToastProps & {
|
||||
|
||||
interface ToastProviderContextValue {
|
||||
toasts: ToastEntry[]
|
||||
toast: (props: Omit<ToastEntry, "id">) => string
|
||||
toast: (props: Omit<ToastEntry, 'id'>) => string
|
||||
dismiss: (id: string) => void
|
||||
dismissAll: () => void
|
||||
}
|
||||
|
||||
const ToastContext = React.createContext<ToastProviderContextValue | null>(null)
|
||||
|
||||
function ToastProvider({ children, position = "bottom-right" }: { children: React.ReactNode; position?: ToastViewportProps["position"] }) {
|
||||
const [toasts, setToasts] = React.useState<ToastEntry[]>([])
|
||||
|
||||
const dismiss = React.useCallback((id: string) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id))
|
||||
}, [])
|
||||
|
||||
const dismissAll = React.useCallback(() => {
|
||||
setToasts([])
|
||||
}, [])
|
||||
|
||||
const toast = React.useCallback(
|
||||
(props: Omit<ToastEntry, "id">) => {
|
||||
const id = Math.random().toString(36).slice(2)
|
||||
const duration = props.duration ?? 5000
|
||||
setToasts((prev) => [...prev, { ...props, id }])
|
||||
if (duration > 0) {
|
||||
setTimeout(() => dismiss(id), duration)
|
||||
}
|
||||
return id
|
||||
},
|
||||
[dismiss]
|
||||
)
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ toasts, toast, dismiss, dismissAll }}>
|
||||
{children}
|
||||
<ToastViewport position={position}>
|
||||
{toasts.map((t) => {
|
||||
const { id, duration: _duration, ...rest } = t
|
||||
return (
|
||||
<Toast key={id} {...rest} onClose={() => dismiss(id)} />
|
||||
)
|
||||
})}
|
||||
</ToastViewport>
|
||||
</ToastContext.Provider>
|
||||
)
|
||||
function ToastProvider({ children }: { children: React.ReactNode; position?: ToastViewportProps['position'] }) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const ctx = React.useContext(ToastContext)
|
||||
if (!ctx) throw new Error("useToast must be used within ToastProvider")
|
||||
return ctx
|
||||
function useToast(): ToastProviderContextValue {
|
||||
return {
|
||||
toasts: [],
|
||||
toast: (props: Omit<ToastEntry, 'id'>) => {
|
||||
const id = Math.random().toString(36).slice(2)
|
||||
notifications.show({
|
||||
id,
|
||||
title: props.title,
|
||||
message: props.description ?? '',
|
||||
color: variantColors[props.variant ?? 'default'],
|
||||
autoClose: props.duration === 0 ? false : (props.duration ?? 5000),
|
||||
})
|
||||
return id
|
||||
},
|
||||
dismiss: (id: string) => {
|
||||
notifications.hide(id)
|
||||
},
|
||||
dismissAll: () => {
|
||||
notifications.cleanQueue()
|
||||
notifications.clean()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export { Toast, ToastProvider, ToastViewport, toastVariants, useToast }
|
||||
|
||||
Reference in New Issue
Block a user