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
+114 -197
View File
@@ -2,6 +2,17 @@
Eres un arquitecto frontend experto. Esta skill se activa cuando el usuario pide crear un proyecto frontend, una app con UI, un componente nuevo, o una feature frontend. Tu trabajo es garantizar que TODO el frontend se construya usando el sistema de funciones reutilizables del registry y las mejores practicas actuales.
## Stack
- **pnpm** — gestor de paquetes
- **React 19** — UI library
- **Vite 8** — build tool
- **Mantine v9** — component library + styling (props, no CSS manual)
- **Phosphor Icons** — `@phosphor-icons/react`
- **Recharts** — charts (via `@mantine/charts`)
**NO usar:** Tailwind, shadcn, CVA, clsx, cn(), lucide-react, styled-components, emotion, CSS-in-JS runtime.
---
## PASO 1: Consultar el registry (OBLIGATORIO)
@@ -56,11 +67,12 @@ apps/{nombre}/
package.json
vite.config.ts
tsconfig.json
postcss.config.cjs
index.html
src/
main.tsx # Entry point
App.tsx # Root con ThemeProvider + Router
app.css # Tokens CSS — NUNCA hardcodear colores
main.tsx # Entry point con MantineProvider
App.tsx # Root con Router
app.css # Minimal (font-smoothing solo)
features/ # Feature-based co-location
{feature}/
components/ # Componentes del feature
@@ -87,21 +99,20 @@ apps/{nombre}/
"preview": "vite preview --host"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.577.0",
"@mantine/core": "^9.0.0",
"@mantine/hooks": "^9.0.0",
"@mantine/notifications": "^9.0.0",
"@phosphor-icons/react": "^2.1.10",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^2.15.0",
"tailwind-merge": "^3.5.0"
"react-dom": "^19.2.4"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"tailwindcss": "^4.2.2",
"postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "~5.9.3",
"vite": "^8.0.0"
}
@@ -109,10 +120,10 @@ apps/{nombre}/
```
Agregar dependencias extras segun necesidad:
- **Charts**: `@mantine/charts`, `recharts`
- **Tablas**: `@tanstack/react-table`
- **Charts**: `recharts`
- **Iconos extra**: `@phosphor-icons/react`
- **Forms**: `react-hook-form`, `@hookform/resolvers`, `zod`
- **Dates**: `@mantine/dates`, `dayjs`
- **Router**: `react-router` o `@tanstack/react-router`
- **State**: `zustand` (client state), `@tanstack/react-query` (server state)
- **Wails**: los hooks de Wails ya estan en `@fn_library` (useWailsQuery, useWailsMutation, useWailsStream, useWailsEvent, WailsProvider)
@@ -122,11 +133,10 @@ Agregar dependencias extras segun necesidad:
```ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { resolve } from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
@@ -134,6 +144,9 @@ export default defineConfig({
},
dedupe: ['react', 'react-dom'],
},
css: {
postcss: resolve(__dirname, './postcss.config.cjs'),
},
build: {
target: 'es2022',
rollupOptions: {
@@ -147,108 +160,32 @@ export default defineConfig({
})
```
### postcss.config.cjs base
```js
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
```
### app.css base
```css
@import "tailwindcss";
@theme inline {
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
}
/* Dark theme (default) */
:root,
[data-theme="dark"] {
--background: oklch(8% 0.015 260);
--foreground: oklch(95% 0.01 260);
--muted: oklch(18% 0.02 260);
--muted-foreground: oklch(60% 0.02 260);
--border: oklch(15% 0.01 260);
--primary: oklch(65% 0.22 260);
--primary-foreground: oklch(98% 0.01 260);
--secondary: oklch(20% 0.02 260);
--secondary-foreground: oklch(95% 0.01 260);
--accent: oklch(18% 0.03 260);
--accent-foreground: oklch(95% 0.01 260);
--destructive: oklch(55% 0.22 25);
--destructive-foreground: oklch(98% 0.01 260);
--card: oklch(11% 0.015 260);
--card-foreground: oklch(95% 0.01 260);
--popover: oklch(12% 0.015 260);
--popover-foreground: oklch(95% 0.01 260);
--ring: oklch(65% 0.22 260);
--input: oklch(22% 0.02 260);
--radius: 0.5rem;
--chart-1: oklch(62% 0.19 260);
--chart-2: oklch(65% 0.2 155);
--chart-3: oklch(75% 0.18 85);
--chart-4: oklch(60% 0.22 25);
--chart-5: oklch(60% 0.2 300);
}
/* Light theme */
[data-theme="light"] {
--background: oklch(99% 0.005 260);
--foreground: oklch(15% 0.01 260);
--muted: oklch(95% 0.01 260);
--muted-foreground: oklch(45% 0.02 260);
--border: oklch(90% 0.01 260);
--primary: oklch(50% 0.22 260);
--primary-foreground: oklch(98% 0.01 260);
--secondary: oklch(95% 0.01 260);
--secondary-foreground: oklch(20% 0.01 260);
--accent: oklch(95% 0.02 260);
--accent-foreground: oklch(20% 0.01 260);
--destructive: oklch(55% 0.22 25);
--destructive-foreground: oklch(98% 0.01 260);
--card: oklch(100% 0 0);
--card-foreground: oklch(15% 0.01 260);
--popover: oklch(100% 0 0);
--popover-foreground: oklch(15% 0.01 260);
--ring: oklch(50% 0.22 260);
--input: oklch(90% 0.01 260);
--radius: 0.5rem;
--chart-1: oklch(55% 0.22 260);
--chart-2: oklch(55% 0.2 155);
--chart-3: oklch(65% 0.18 85);
--chart-4: oklch(55% 0.22 25);
--chart-5: oklch(55% 0.2 300);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
/* Minimal — Mantine handles all theming via MantineProvider */
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (prefers-reduced-motion: reduce) {
@@ -259,18 +196,33 @@ export default defineConfig({
}
```
### App.tsx base
### main.tsx base
```tsx
import { ThemeProvider } from '@fn_library'
import '@mantine/core/styles.css'
import '@mantine/notifications/styles.css'
import './app.css'
export default function App() {
return (
<ThemeProvider defaultTheme="dark">
{/* Router y contenido aqui */}
</ThemeProvider>
)
}
import React from 'react'
import ReactDOM from 'react-dom/client'
import { MantineProvider, createTheme } from '@mantine/core'
import { Notifications } from '@mantine/notifications'
import App from './App'
const theme = createTheme({
primaryColor: 'blue',
defaultRadius: 'md',
// Customize colors, fonts, etc. here
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MantineProvider theme={theme} defaultColorScheme="dark">
<Notifications />
<App />
</MantineProvider>
</React.StrictMode>,
)
```
### Despues del scaffold
@@ -287,17 +239,16 @@ Para componentes nuevos que van al registry en `frontend/functions/`.
### Reglas de implementacion
1. **Headless first**: usar `@base-ui/react` como primitivo si el componente es interactivo (dialog, select, tooltip, etc.)
2. **CVA para variantes**: SIEMPRE usar `class-variance-authority` para definir variantes
3. **cn() para clases**: SIEMPRE usar `cn()` de `frontend/functions/core/cn.ts` para componer classNames
4. **CSS variables**: NUNCA hex/rgb/oklch inline en el componente — solo clases Tailwind que mapean a CSS variables (`bg-primary`, `text-muted-foreground`, `border-border`)
5. **Props tipadas**: usar `React.ComponentPropsWithoutRef<"element">` para HTML props spreading
1. **Mantine first**: wrappear componentes de Mantine. Solo crear desde cero si Mantine no tiene equivalente.
2. **Styling via props**: usar props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, `gap`, etc.) y el style system. NUNCA clases CSS manuales ni Tailwind.
3. **CSS variables de Mantine**: si necesitas styles inline, usar `var(--mantine-color-*)`, `var(--mantine-spacing-*)`, etc.
4. **Iconos**: usar `@phosphor-icons/react`, no lucide-react ni @tabler/icons-react.
5. **Props tipadas**: usar `React.ComponentPropsWithoutRef<"element">` para HTML props spreading.
6. **Accesibilidad**:
- Elementos semanticos: `<button>` para acciones, `<a>` para navegacion, `<dialog>` para modales
- Elementos semanticos: `<button>` para acciones, `<a>` para navegacion
- NUNCA `<div onClick>` para elementos interactivos
- `aria-label` o `aria-labelledby` en todo componente interactivo
- `aria-label` en botones de solo icono
- `aria-invalid` + `aria-describedby` en inputs con error
- `role="status"` para loading states
- Focus management en modales/popovers
7. **Discriminated unions** cuando las props cambian segun variante:
@@ -311,54 +262,19 @@ type Props = { size?: 'sm' | 'md' | 'lg'; children: React.ReactNode } & (
### Patron de archivo .tsx
```tsx
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../core/cn'
import { Select, type SelectProps } from '@mantine/core'
const componentVariants = cva(
'base-classes-here', // clases base
{
variants: {
variant: {
default: 'classes...',
secondary: 'classes...',
},
size: {
sm: 'classes...',
md: 'classes...',
lg: 'classes...',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
}
)
interface ComponentProps
extends React.ComponentPropsWithoutRef<'div'>,
VariantProps<typeof componentVariants> {
// props adicionales con JSDoc
/** Descripcion de la prop */
// Re-export con defaults o logica adicional si necesario
interface MySelectProps extends Omit<SelectProps, 'xxx'> {
customProp?: string
}
const Component = React.forwardRef<HTMLDivElement, ComponentProps>(
({ className, variant, size, customProp, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(componentVariants({ variant, size }), className)}
{...props}
/>
)
}
)
Component.displayName = 'Component'
function MySelect({ customProp, ...props }: MySelectProps) {
return <Select {...props} />
}
export { Component, componentVariants }
export type { ComponentProps }
export { MySelect }
export type { MySelectProps }
```
### Patron de archivo .md
@@ -376,12 +292,12 @@ purity: impure
signature: "ComponentName(props: ComponentProps): JSX.Element"
description: "Descripcion concisa de que hace el componente"
tags: [component, ui, ...]
uses_functions: [cn_ts_core]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", "class-variance-authority"]
imports: ["@mantine/core"]
tested: false
tests: []
test_file_path: ""
@@ -391,14 +307,10 @@ props:
type: "'default' | 'secondary'"
required: false
description: "Estilo visual"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: [default, secondary]
variant: [default]
---
## Ejemplo
@@ -493,7 +405,7 @@ function useFeatureData() {
```tsx
import { lazy, Suspense } from 'react'
import { Skeleton } from '@fn_library'
import { Skeleton } from '@mantine/core'
const FeaturePage = lazy(() => import('./features/feature/components/FeaturePage'))
@@ -501,7 +413,7 @@ function AppRoutes() {
return (
<Routes>
<Route path="/feature" element={
<Suspense fallback={<Skeleton className="h-screen w-full" />}>
<Suspense fallback={<Skeleton height="100vh" />}>
<FeaturePage />
</Suspense>
} />
@@ -517,14 +429,19 @@ function AppRoutes() {
Antes de dar por terminado cualquier trabajo frontend, verificar:
### Colores y estilos
- [ ] CERO colores hardcodeados (no hex, no rgb, no oklch inline en componentes)
- [ ] Solo clases Tailwind mapeadas a CSS variables: `bg-primary`, `text-foreground`, `border-border`, etc.
- [ ] `cn()` usado para merge de clases en todo componente
- [ ] CVA usado para variantes (no condicionales manuales con ternarios)
- [ ] CERO colores hardcodeados en componentes (no hex, no rgb inline)
- [ ] Styling via props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, `gap`, etc.)
- [ ] Si se necesitan styles inline, usar CSS variables de Mantine (`var(--mantine-color-*)`)
- [ ] NO clases CSS manuales, NO Tailwind, NO cn(), NO CVA
### Componentes del registry
- [ ] Verificado que no se esta recreando algo que ya existe en `@fn_library` (`frontend/functions/ui/`)
- [ ] Componentes de `@fn_library` usados donde aplica: Alert, Badge, Button, Card, Dialog, Input, Label, Select, SimpleSelect, Skeleton, Sparkline, Tabs, Tooltip, FormField, PageHeader, ProgressBar, KPICard, ThemeProvider, DashboardLayout, DataTable, charts (AreaChart, BarChart, LineChart, PieChart, ChartContainer), hooks Wails (useWailsQuery, useWailsMutation, useWailsStream, useWailsEvent)
- [ ] Componentes de `@fn_library` usados donde aplica: Card, Select, SimpleSelect, KPICard, Sparkline, DashboardLayout, DataTable, charts, hooks Wails
- [ ] Componentes de Mantine usados directamente donde `@fn_library` no tiene wrapper: Button, TextInput, Table, Alert, Badge, Skeleton, Tabs, Tooltip, Group, Stack, Grid, Box, Paper, AppShell, Container
### Iconos
- [ ] Usando `@phosphor-icons/react` para iconos
- [ ] NO lucide-react, NO @tabler/icons-react
### TypeScript
- [ ] Props interfaces con `React.ComponentPropsWithoutRef` para HTML spreading
@@ -533,7 +450,7 @@ Antes de dar por terminado cualquier trabajo frontend, verificar:
- [ ] No `any` — usar `unknown` + type guards si es necesario
### Accesibilidad
- [ ] Elementos semanticos (button, a, dialog — no div onClick)
- [ ] Elementos semanticos (button, a — no div onClick)
- [ ] `aria-label` en botones de solo icono
- [ ] `aria-invalid` + `aria-describedby` en inputs con validacion
- [ ] Focus trap en modales y popovers
@@ -555,15 +472,15 @@ Antes de dar por terminado cualquier trabajo frontend, verificar:
## ANTI-PATRONES (nunca hacer)
1. **`<div onClick={...}>`** → usar `<button>` o Base-UI primitivo
2. **`style={{ color: '#3b82f6' }}`** → usar `className="text-primary"`
3. **`import Button from './MyButton'`** cuando existe en la lib → usar `import { Button } from '@fn_library'`
1. **`<div onClick={...}>`** → usar `<button>` o componente Mantine
2. **`style={{ color: '#3b82f6' }}`** → usar prop `c="blue"` o `var(--mantine-color-blue-6)`
3. **`import Button from './MyButton'`** cuando existe en Mantine → usar `import { Button } from '@mantine/core'`
4. **Estado global para todo** → segmentar: server state (React Query), client state (Zustand), form state (React Hook Form), URL state (search params)
5. **`index.ts` en la raiz de `src/`** que re-exporta todo → mata tree-shaking
6. **`// @ts-ignore`** → arreglar el tipo
7. **CSS-in-JS runtime** (styled-components, emotion) → usar Tailwind
8. **Instalar shadcn/ui como dependencia** → los componentes ya estan en el registry, usar `@fn_library`
9. **Crear utilidades que ya existen**: `cn()`, `getSeriesColor()`, `ChartContainer`, `ThemeProvider` ya estan en `@fn_library`
10. **Colores de chart hardcodeados** → usar `--chart-1` a `--chart-5` o `getSeriesColor()`
7. **CSS-in-JS runtime** (styled-components, emotion) → usar props de Mantine
8. **Tailwind, CVA, cn(), clsx** → usar props de Mantine y su style system
9. **Crear utilidades que ya existen**: `getSeriesColor()`, `ChartContainer`, `DashboardLayout`, `DataTable` ya estan en `@fn_library`
10. **Colores de chart hardcodeados** → usar `@mantine/charts` color system o `getSeriesColor()`
$ARGUMENTS
+9 -1
View File
@@ -1,3 +1,11 @@
En todos los frontends se usan los componentes de `@fn_library` (alias a `frontend/functions/ui/`) antes que elementos HTML nativos o librerias externas.
En todos los frontends se usa el sistema de temas basado en CSS variables (`--background`, `--foreground`, `--input`, `--border`, `--popover`, etc.) definidas en `app.css`. Los componentes deben leer estas variables para adaptarse al tema activo. Nunca hardcodear colores.
El sistema de UI es Mantine v9. Todos los componentes de @fn_library wrappean componentes de Mantine.
**Theming:** Cada app define su tema con `createTheme()` de `@mantine/core` y lo pasa a `MantineProvider` (o `FnMantineProvider` de @fn_library). No se usan CSS variables custom — Mantine genera las suyas automaticamente (`--mantine-color-*`).
**Styling:** No se usa Tailwind, CVA, cn(), ni clases CSS manuales. Los componentes se estilizan con props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, etc.) y el style system de Mantine.
**Iconos:** Se usa `@tabler/icons-react` (el set nativo de Mantine), no lucide-react.
**Layout:** Se usan los componentes de layout de Mantine: `Group`, `Stack`, `Grid`, `Flex`, `SimpleGrid`, `AppShell`, `Container`, `Box`, `Paper`.