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:
2026-04-06 23:46:44 +02:00
parent 4b2bb6998a
commit 97a3c84625
163 changed files with 6008 additions and 6310 deletions
+70 -59
View File
@@ -1,5 +1,5 @@
import * as React from 'react'
import { cn } from '../core/cn'
import { Stack, Group, Title, Text, Paper, TextInput, Textarea, Switch, NativeSelect, Button, Box, Anchor } from '@mantine/core'
interface SettingField {
key: string
@@ -31,81 +31,92 @@ export function settingsPage({
subtitle,
sections,
onSave,
className,
}: SettingsPageProps): React.ReactElement {
return (
<div className={cn('space-y-6', className)}>
<Stack gap="lg">
{/* Header */}
<div className="border-b pb-4">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
</div>
<Box pb="md" style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
<Title order={2}>{title}</Title>
{subtitle && <Text size="sm" c="dimmed">{subtitle}</Text>}
</Box>
{/* Tabs navigation */}
<div className="flex gap-6">
<nav className="hidden w-48 shrink-0 md:block">
<div className="space-y-1">
{/* Content with sidebar nav */}
<Group align="flex-start" gap="xl">
<Box w={192} visibleFrom="md" style={{ flexShrink: 0 }}>
<Stack gap={4}>
{sections.map((section) => (
<a
<Anchor
key={section.id}
href={`#${section.id}`}
className="block rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground"
underline="never"
size="sm"
fw={500}
c="dimmed"
py={6}
px="sm"
style={{ display: 'block', borderRadius: 'var(--mantine-radius-sm)' }}
>
{section.title}
</a>
</Anchor>
))}
</div>
</nav>
</Stack>
</Box>
{/* Sections */}
<div className="flex-1 space-y-8">
<Stack gap="xl" style={{ flex: 1 }}>
{sections.map((section) => (
<div key={section.id} id={section.id} className="space-y-4">
<div>
<h2 className="text-lg font-medium">{section.title}</h2>
{section.description && <p className="text-sm text-muted-foreground">{section.description}</p>}
</div>
<div className="space-y-4 rounded-lg border p-4">
{section.fields.map((field) => (
<div key={field.key} className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-0.5">
<label className="text-sm font-medium">{field.label}</label>
{field.description && <p className="text-xs text-muted-foreground">{field.description}</p>}
</div>
<div className="w-full sm:w-64">
{field.type === 'toggle' ? (
<button className={cn(
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors',
field.value ? 'bg-primary' : 'bg-input'
)}>
<span className={cn('pointer-events-none block size-4 rounded-full bg-background shadow-lg ring-0 transition-transform', field.value ? 'translate-x-4' : 'translate-x-0')} />
</button>
) : field.type === 'select' ? (
<select className="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm">
{field.options?.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
) : field.type === 'textarea' ? (
<textarea className="w-full rounded-lg border border-input bg-transparent px-2.5 py-1.5 text-sm" rows={3} placeholder={field.placeholder} defaultValue={String(field.value ?? '')} />
) : (
<input type={field.type} className="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm" placeholder={field.placeholder} defaultValue={String(field.value ?? '')} />
)}
</div>
</div>
))}
</div>
</div>
<Stack key={section.id} id={section.id} gap="md">
<Box>
<Text size="lg" fw={500}>{section.title}</Text>
{section.description && <Text size="sm" c="dimmed">{section.description}</Text>}
</Box>
<Paper withBorder p="md" radius="md">
<Stack gap="md">
{section.fields.map((field) => (
<Group key={field.key} justify="space-between" align="center" wrap="wrap" gap="md">
<Stack gap={2} style={{ flex: '1 1 auto' }}>
<Text size="sm" fw={500}>{field.label}</Text>
{field.description && <Text size="xs" c="dimmed">{field.description}</Text>}
</Stack>
<Box w={{ base: '100%', sm: 256 }}>
{field.type === 'toggle' ? (
<Switch defaultChecked={!!field.value} />
) : field.type === 'select' ? (
<NativeSelect
data={field.options?.map(o => ({ label: o.label, value: o.value })) ?? []}
size="xs"
/>
) : field.type === 'textarea' ? (
<Textarea
rows={3}
placeholder={field.placeholder}
defaultValue={String(field.value ?? '')}
size="xs"
/>
) : (
<TextInput
type={field.type}
placeholder={field.placeholder}
defaultValue={String(field.value ?? '')}
size="xs"
/>
)}
</Box>
</Group>
))}
</Stack>
</Paper>
</Stack>
))}
{onSave && (
<div className="flex justify-end border-t pt-4">
<button className="inline-flex h-8 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/80">
Save changes
</button>
</div>
<Group justify="flex-end" pt="md" style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}>
<Button size="xs">Save changes</Button>
</Group>
)}
</div>
</div>
</div>
</Stack>
</Group>
</Stack>
)
}