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,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { SearchIcon, XIcon } from "lucide-react"
|
||||
import { cn } from "../core/cn"
|
||||
import * as React from 'react'
|
||||
import { TextInput, Text, Box, ScrollArea } from '@mantine/core'
|
||||
import { IconSearch } from '@tabler/icons-react'
|
||||
|
||||
interface CommandItem {
|
||||
interface CommandItemData {
|
||||
value: string
|
||||
label: string
|
||||
description?: string
|
||||
@@ -12,7 +12,7 @@ interface CommandItem {
|
||||
}
|
||||
|
||||
interface CommandProps {
|
||||
items: CommandItem[]
|
||||
items: CommandItemData[]
|
||||
value?: string
|
||||
onValueChange?: (value: string) => void
|
||||
placeholder?: string
|
||||
@@ -22,122 +22,107 @@ interface CommandProps {
|
||||
listClassName?: string
|
||||
}
|
||||
|
||||
function Command({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
|
||||
function Command({ className, children, ...props }: React.ComponentPropsWithoutRef<'div'>) {
|
||||
return <Box data-slot="command" className={className} {...props}>{children}</Box>
|
||||
}
|
||||
|
||||
function CommandInput({ className, value, onChange, placeholder, ...props }: {
|
||||
className?: string
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
placeholder?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command"
|
||||
className={cn("flex h-full w-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground", className)}
|
||||
<TextInput
|
||||
data-slot="command-input"
|
||||
leftSection={<IconSearch size={16} />}
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
styles={{ input: { border: 'none', borderBottom: '1px solid var(--mantine-color-default-border)' } }}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({ className, ...props }: React.ComponentPropsWithoutRef<"input">) {
|
||||
function CommandList({ className, children }: { className?: string; children?: React.ReactNode }) {
|
||||
return (
|
||||
<div data-slot="command-input-wrapper" className="flex items-center border-b px-3">
|
||||
<SearchIcon className="mr-2 size-4 shrink-0 text-muted-foreground" />
|
||||
<input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none",
|
||||
"placeholder:text-muted-foreground",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
<ScrollArea.Autosize mah={300} data-slot="command-list" className={className}>
|
||||
{children}
|
||||
</ScrollArea.Autosize>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
|
||||
function CommandEmpty({ className, children }: { className?: string; children?: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-list"
|
||||
className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
<Text ta="center" c="dimmed" size="sm" py="xl" data-slot="command-empty" className={className}>
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
|
||||
function CommandGroup({ className, heading, children }: { className?: string; heading?: string; children?: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-empty"
|
||||
className={cn("py-6 text-center text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
<Box data-slot="command-group" p={4} className={className}>
|
||||
{heading && <Text size="xs" fw={500} c="dimmed" px="sm" py={6}>{heading}</Text>}
|
||||
<div>{children}</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({ className, heading, ...props }: React.ComponentPropsWithoutRef<"div"> & { heading?: string }) {
|
||||
return (
|
||||
<div data-slot="command-group" className={cn("overflow-hidden p-1 text-foreground", className)}>
|
||||
{heading && (
|
||||
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">{heading}</div>
|
||||
)}
|
||||
<div {...props} />
|
||||
</div>
|
||||
)
|
||||
function CommandSeparator({ className }: { className?: string }) {
|
||||
return <Box data-slot="command-separator" h={1} bg="var(--mantine-color-default-border)" mx={-4} className={className} />
|
||||
}
|
||||
|
||||
function CommandSeparator({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-separator"
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface CommandItemProps extends React.ComponentPropsWithoutRef<"div"> {
|
||||
function CommandItem({ className, selected, disabled, onSelect, children }: {
|
||||
className?: string
|
||||
selected?: boolean
|
||||
disabled?: boolean
|
||||
onSelect?: () => void
|
||||
}
|
||||
|
||||
function CommandItem({ className, selected, disabled, onSelect, ...props }: CommandItemProps) {
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
<Box
|
||||
data-slot="command-item"
|
||||
data-selected={selected}
|
||||
aria-disabled={disabled}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
onClick={!disabled ? onSelect : undefined}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
disabled && "pointer-events-none opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
px="sm"
|
||||
py={6}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
borderRadius: 'var(--mantine-radius-sm)',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
backgroundColor: selected ? 'var(--mantine-color-default-hover)' : undefined,
|
||||
fontSize: 'var(--mantine-font-size-sm)',
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function CommandShortcut({ className, children }: { className?: string; children?: React.ReactNode }) {
|
||||
return <Text span size="xs" c="dimmed" ml="auto" className={className}>{children}</Text>
|
||||
}
|
||||
|
||||
function CommandSearch({
|
||||
items,
|
||||
value,
|
||||
onValueChange,
|
||||
placeholder = "Search...",
|
||||
emptyMessage = "No results found.",
|
||||
placeholder = 'Search...',
|
||||
emptyMessage = 'No results found.',
|
||||
className,
|
||||
}: CommandProps) {
|
||||
const [query, setQuery] = React.useState("")
|
||||
const [selectedValue, setSelectedValue] = React.useState(value ?? "")
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [selectedValue, setSelectedValue] = React.useState(value ?? '')
|
||||
|
||||
const filtered = React.useMemo(() => {
|
||||
if (!query) return items
|
||||
@@ -151,9 +136,9 @@ function CommandSearch({
|
||||
}, [items, query])
|
||||
|
||||
const groups = React.useMemo(() => {
|
||||
const map = new Map<string, CommandItem[]>()
|
||||
const map = new Map<string, CommandItemData[]>()
|
||||
for (const item of filtered) {
|
||||
const key = item.group ?? ""
|
||||
const key = item.group ?? ''
|
||||
if (!map.has(key)) map.set(key, [])
|
||||
map.get(key)!.push(item)
|
||||
}
|
||||
@@ -185,10 +170,10 @@ function CommandSearch({
|
||||
disabled={item.disabled}
|
||||
onSelect={() => handleSelect(item.value)}
|
||||
>
|
||||
{item.icon && <span className="shrink-0">{item.icon}</span>}
|
||||
{item.icon && <span>{item.icon}</span>}
|
||||
<span>{item.label}</span>
|
||||
{item.description && (
|
||||
<span className="ml-auto text-xs text-muted-foreground">{item.description}</span>
|
||||
<Text span size="xs" c="dimmed" ml="auto">{item.description}</Text>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
@@ -201,4 +186,4 @@ function CommandSearch({
|
||||
}
|
||||
|
||||
export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut }
|
||||
export type { CommandItem, CommandProps }
|
||||
export type { CommandItemData, CommandProps }
|
||||
|
||||
Reference in New Issue
Block a user