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:
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react'
|
||||
import { cn } from '../core/cn'
|
||||
import { Stack, Group, Title, Text, Paper, Table, Button, ActionIcon, Center } from '@mantine/core'
|
||||
import { IconPlus, IconPencil, IconTrash } from '@tabler/icons-react'
|
||||
|
||||
interface CrudField {
|
||||
key: string
|
||||
@@ -37,83 +38,83 @@ export function crudPage<T extends Record<string, unknown>>({
|
||||
onEdit,
|
||||
onDelete,
|
||||
actions,
|
||||
className,
|
||||
}: CrudPageProps<T>): React.ReactElement {
|
||||
return (
|
||||
<div className={cn('space-y-6', className)}>
|
||||
<Stack gap="lg">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b pb-4">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
||||
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Group justify="space-between" pb="md" style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
|
||||
<Stack gap={4}>
|
||||
<Title order={2}>{title}</Title>
|
||||
{subtitle && <Text size="sm" c="dimmed">{subtitle}</Text>}
|
||||
</Stack>
|
||||
<Group gap="xs">
|
||||
{actions}
|
||||
{onAdd && (
|
||||
<button className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/80">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 5v14M5 12h14"/></svg>
|
||||
<Button size="xs" leftSection={<IconPlus size={16} />}>
|
||||
Add {title.replace(/s$/, '')}
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Table */}
|
||||
<div className="rounded-lg border">
|
||||
<table className="w-full caption-bottom text-sm">
|
||||
<thead className="border-b bg-muted/50">
|
||||
<tr>
|
||||
<Paper withBorder radius="md">
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{columns.map((col) => (
|
||||
<th key={String(col.key)} className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">
|
||||
<Table.Th key={String(col.key)} fz="sm" fw={500} c="dimmed" px="md" py="sm">
|
||||
{col.label}
|
||||
</th>
|
||||
</Table.Th>
|
||||
))}
|
||||
{(onEdit || onDelete) && (
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-muted-foreground">Actions</th>
|
||||
<Table.Th ta="right" fz="sm" fw={500} c="dimmed" px="md" py="sm">Actions</Table.Th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columns.length + (onEdit || onDelete ? 1 : 0)} className="h-24 text-center text-muted-foreground">
|
||||
No items yet.
|
||||
</td>
|
||||
</tr>
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={columns.length + (onEdit || onDelete ? 1 : 0)}>
|
||||
<Center h={96}>
|
||||
<Text c="dimmed">No items yet.</Text>
|
||||
</Center>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
data.map((row, i) => (
|
||||
<tr key={i} className="hover:bg-muted/50">
|
||||
<Table.Tr key={i}>
|
||||
{columns.map((col) => (
|
||||
<td key={String(col.key)} className="px-4 py-3 align-middle">
|
||||
<Table.Td key={String(col.key)} px="md" py="sm" style={{ verticalAlign: 'middle' }}>
|
||||
{col.render ? col.render(row[col.key], row) : String(row[col.key] ?? '')}
|
||||
</td>
|
||||
</Table.Td>
|
||||
))}
|
||||
{(onEdit || onDelete) && (
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Table.Td px="md" py="sm" ta="right">
|
||||
<Group gap={4} justify="flex-end">
|
||||
{onEdit && (
|
||||
<button onClick={() => onEdit(row)} className="inline-flex size-7 items-center justify-center rounded-md hover:bg-muted">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
</button>
|
||||
<ActionIcon variant="subtle" size="sm" onClick={() => onEdit(row)}>
|
||||
<IconPencil size={14} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{onDelete && (
|
||||
<button onClick={() => onDelete(row)} className="inline-flex size-7 items-center justify-center rounded-md text-destructive hover:bg-destructive/10">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
</button>
|
||||
<ActionIcon variant="subtle" color="red" size="sm" onClick={() => onDelete(row)}>
|
||||
<IconTrash size={14} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
)}
|
||||
</tr>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
{/* Form fields definition (for agent use — renders a form preview) */}
|
||||
<div className="hidden" data-slot="crud-form-schema" data-fields={JSON.stringify(fields)} />
|
||||
</div>
|
||||
{/* Form fields definition (for agent use) */}
|
||||
<div style={{ display: 'none' }} data-slot="crud-form-schema" data-fields={JSON.stringify(fields)} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user