2d108c295a
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>
487 lines
14 KiB
Markdown
487 lines
14 KiB
Markdown
# /frontend — Skill para proyectos frontend
|
|
|
|
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)
|
|
|
|
Antes de escribir una sola linea de codigo, consulta registry.db para saber que componentes, funciones y tipos frontend ya existen:
|
|
|
|
```bash
|
|
# Componentes y funciones frontend disponibles
|
|
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE lang IN ('ts','typescript') ORDER BY domain, name;"
|
|
|
|
# Tipos frontend disponibles
|
|
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE lang IN ('ts','typescript') ORDER BY domain, name;"
|
|
|
|
# Busqueda FTS5 si buscas algo especifico
|
|
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:chart* OR description:chart*') ORDER BY name;"
|
|
```
|
|
|
|
Tambien lista los archivos reales en disco ya que no todos estan indexados aun:
|
|
|
|
```bash
|
|
ls frontend/functions/ui/ # Componentes React
|
|
ls frontend/functions/core/ # Utilidades TS puras
|
|
ls frontend/types/ # Tipos
|
|
```
|
|
|
|
**REGLA:** Si un componente ya existe en `frontend/functions/ui/` (alias `@fn_library`), USALO. Nunca recrear lo que ya existe.
|
|
|
|
---
|
|
|
|
## PASO 2: Determinar el tipo de trabajo
|
|
|
|
### A) App nueva en `apps/`
|
|
Ir a → Seccion SCAFFOLD APP
|
|
|
|
### B) Componente nuevo para el registry
|
|
Ir a → Seccion CREAR COMPONENTE
|
|
|
|
### C) Feature en app existente
|
|
Ir a → Seccion CREAR FEATURE
|
|
|
|
---
|
|
|
|
## SCAFFOLD APP
|
|
|
|
Crear la estructura completa de una app frontend nueva en `apps/{nombre}/frontend/`.
|
|
|
|
### Estructura obligatoria
|
|
|
|
```
|
|
apps/{nombre}/
|
|
frontend/
|
|
package.json
|
|
vite.config.ts
|
|
tsconfig.json
|
|
postcss.config.cjs
|
|
index.html
|
|
src/
|
|
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
|
|
hooks/ # Hooks del feature
|
|
types.ts # Tipos del feature
|
|
index.ts # Barrel export publico
|
|
components/ # Componentes compartidos de esta app (no reutilizables)
|
|
hooks/ # Hooks compartidos
|
|
lib/ # Utilidades, API client
|
|
types/ # Tipos globales de la app
|
|
```
|
|
|
|
### package.json base
|
|
|
|
```json
|
|
{
|
|
"name": "{nombre}",
|
|
"private": true,
|
|
"version": "0.0.1",
|
|
"type": "module",
|
|
"scripts": {
|
|
"dev": "vite --host",
|
|
"build": "tsc -b && vite build",
|
|
"preview": "vite preview --host"
|
|
},
|
|
"dependencies": {
|
|
"@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"
|
|
},
|
|
"devDependencies": {
|
|
"@types/react": "^19.2.14",
|
|
"@types/react-dom": "^19.2.3",
|
|
"@vitejs/plugin-react": "^6.0.0",
|
|
"postcss": "^8.5.8",
|
|
"postcss-preset-mantine": "^1.18.0",
|
|
"postcss-simple-vars": "^7.0.1",
|
|
"typescript": "~5.9.3",
|
|
"vite": "^8.0.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
Agregar dependencias extras segun necesidad:
|
|
- **Charts**: `@mantine/charts`, `recharts`
|
|
- **Tablas**: `@tanstack/react-table`
|
|
- **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)
|
|
|
|
### vite.config.ts base
|
|
|
|
```ts
|
|
import { defineConfig } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import { resolve } from 'path'
|
|
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
resolve: {
|
|
alias: {
|
|
'@': resolve(__dirname, './src'),
|
|
'@fn_library': resolve(__dirname, '../../../frontend/functions/ui'),
|
|
},
|
|
dedupe: ['react', 'react-dom'],
|
|
},
|
|
css: {
|
|
postcss: resolve(__dirname, './postcss.config.cjs'),
|
|
},
|
|
build: {
|
|
target: 'es2022',
|
|
rollupOptions: {
|
|
output: {
|
|
manualChunks: {
|
|
'react-vendor': ['react', 'react-dom'],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
### 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
|
|
/* Minimal — Mantine handles all theming via MantineProvider */
|
|
html {
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*, *::before, *::after {
|
|
animation-duration: 0.01ms !important;
|
|
transition-duration: 0.01ms !important;
|
|
}
|
|
}
|
|
```
|
|
|
|
### main.tsx base
|
|
|
|
```tsx
|
|
import '@mantine/core/styles.css'
|
|
import '@mantine/notifications/styles.css'
|
|
import './app.css'
|
|
|
|
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
|
|
|
|
```bash
|
|
cd apps/{nombre}/frontend && pnpm install
|
|
```
|
|
|
|
---
|
|
|
|
## CREAR COMPONENTE
|
|
|
|
Para componentes nuevos que van al registry en `frontend/functions/`.
|
|
|
|
### Reglas de implementacion
|
|
|
|
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
|
|
- NUNCA `<div onClick>` para elementos interactivos
|
|
- `aria-label` en botones de solo icono
|
|
- `aria-invalid` + `aria-describedby` en inputs con error
|
|
- Focus management en modales/popovers
|
|
7. **Discriminated unions** cuando las props cambian segun variante:
|
|
|
|
```tsx
|
|
type Props = { size?: 'sm' | 'md' | 'lg'; children: React.ReactNode } & (
|
|
| { variant: 'link'; href: string; onClick?: never }
|
|
| { variant: 'button'; onClick: () => void; href?: never }
|
|
)
|
|
```
|
|
|
|
### Patron de archivo .tsx
|
|
|
|
```tsx
|
|
import { Select, type SelectProps } from '@mantine/core'
|
|
|
|
// Re-export con defaults o logica adicional si necesario
|
|
interface MySelectProps extends Omit<SelectProps, 'xxx'> {
|
|
customProp?: string
|
|
}
|
|
|
|
function MySelect({ customProp, ...props }: MySelectProps) {
|
|
return <Select {...props} />
|
|
}
|
|
|
|
export { MySelect }
|
|
export type { MySelectProps }
|
|
```
|
|
|
|
### Patron de archivo .md
|
|
|
|
**IMPORTANTE:** El campo `lang` debe ser `ts` (no `typescript`). El indexer solo reconoce `ts`. Los IDs siguen el formato `{name}_ts_{domain}`.
|
|
|
|
```yaml
|
|
---
|
|
name: component_name
|
|
kind: component
|
|
lang: ts
|
|
domain: ui
|
|
version: "1.0.0"
|
|
purity: impure
|
|
signature: "ComponentName(props: ComponentProps): JSX.Element"
|
|
description: "Descripcion concisa de que hace el componente"
|
|
tags: [component, ui, ...]
|
|
uses_functions: []
|
|
uses_types: []
|
|
returns: []
|
|
returns_optional: false
|
|
error_type: ""
|
|
imports: ["@mantine/core"]
|
|
tested: false
|
|
tests: []
|
|
test_file_path: ""
|
|
file_path: "frontend/functions/ui/component_name.tsx"
|
|
props:
|
|
- name: variant
|
|
type: "'default' | 'secondary'"
|
|
required: false
|
|
description: "Estilo visual"
|
|
emits: []
|
|
has_state: false
|
|
framework: react
|
|
variant: [default]
|
|
---
|
|
|
|
## Ejemplo
|
|
|
|
...codigo de ejemplo...
|
|
|
|
## Notas
|
|
|
|
...notas relevantes...
|
|
```
|
|
|
|
### Despues de crear
|
|
|
|
```bash
|
|
./fn index && ./fn show {id}
|
|
```
|
|
|
|
---
|
|
|
|
## CREAR FEATURE
|
|
|
|
Para features dentro de una app existente. Co-location obligatoria.
|
|
|
|
### Estructura
|
|
|
|
```
|
|
src/features/{feature_name}/
|
|
components/
|
|
FeatureMain.tsx # Componente principal
|
|
FeatureDetail.tsx # Sub-componentes
|
|
hooks/
|
|
useFeatureData.ts # Hooks del feature
|
|
types.ts # Tipos locales
|
|
index.ts # Barrel export
|
|
```
|
|
|
|
### Barrel export (index.ts)
|
|
|
|
```ts
|
|
// Solo exportar la API publica del feature
|
|
export { FeatureMain } from './components/FeatureMain'
|
|
export { useFeatureData } from './hooks/useFeatureData'
|
|
export type { FeatureItem, FeatureConfig } from './types'
|
|
```
|
|
|
|
### Patrones de estado obligatorios
|
|
|
|
**Server state** (datos de API/backend):
|
|
```tsx
|
|
// Con @tanstack/react-query
|
|
const queryKeys = {
|
|
all: ['feature'] as const,
|
|
list: (filters: Filters) => [...queryKeys.all, 'list', filters] as const,
|
|
detail: (id: string) => [...queryKeys.all, 'detail', id] as const,
|
|
}
|
|
|
|
function useFeatureList(filters: Filters) {
|
|
return useQuery({
|
|
queryKey: queryKeys.list(filters),
|
|
queryFn: () => fetchFeatureList(filters),
|
|
})
|
|
}
|
|
```
|
|
|
|
**Client state** (UI state compartido):
|
|
```tsx
|
|
// Con Zustand
|
|
import { create } from 'zustand'
|
|
|
|
interface FeatureStore {
|
|
selectedId: string | null
|
|
setSelected: (id: string | null) => void
|
|
}
|
|
|
|
const useFeatureStore = create<FeatureStore>((set) => ({
|
|
selectedId: null,
|
|
setSelected: (id) => set({ selectedId: id }),
|
|
}))
|
|
```
|
|
|
|
**Wails** (apps de escritorio):
|
|
```tsx
|
|
// Usar hooks del registry
|
|
import { useWailsQuery, useWailsMutation } from '@fn_library'
|
|
|
|
function useFeatureData() {
|
|
return useWailsQuery('GetFeatureData', [], { staleTime: 60_000 })
|
|
}
|
|
```
|
|
|
|
### Code splitting por ruta
|
|
|
|
```tsx
|
|
import { lazy, Suspense } from 'react'
|
|
import { Skeleton } from '@mantine/core'
|
|
|
|
const FeaturePage = lazy(() => import('./features/feature/components/FeaturePage'))
|
|
|
|
function AppRoutes() {
|
|
return (
|
|
<Routes>
|
|
<Route path="/feature" element={
|
|
<Suspense fallback={<Skeleton height="100vh" />}>
|
|
<FeaturePage />
|
|
</Suspense>
|
|
} />
|
|
</Routes>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## CHECKLIST DE VALIDACION (ejecutar siempre al final)
|
|
|
|
Antes de dar por terminado cualquier trabajo frontend, verificar:
|
|
|
|
### Colores y estilos
|
|
- [ ] 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: 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
|
|
- [ ] Discriminated unions donde las props varian segun tipo/variante
|
|
- [ ] `as const` para arrays literales y config objects
|
|
- [ ] No `any` — usar `unknown` + type guards si es necesario
|
|
|
|
### Accesibilidad
|
|
- [ ] 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
|
|
- [ ] `prefers-reduced-motion` respetado (ya en app.css base)
|
|
|
|
### Performance
|
|
- [ ] Lazy loading en rutas (`React.lazy` + `Suspense`)
|
|
- [ ] `manualChunks` en vite.config para vendor splitting
|
|
- [ ] Sin barrel exports profundos que maten tree-shaking
|
|
- [ ] Listas largas virtualizadas si >100 items
|
|
|
|
### Estructura
|
|
- [ ] Features co-located: componente + hook + tipos + barrel en el mismo directorio
|
|
- [ ] Un `index.ts` por feature con API publica explicita
|
|
- [ ] Componentes reutilizables de la app en `src/components/`
|
|
- [ ] Tipos compartidos en `src/types/`
|
|
|
|
---
|
|
|
|
## ANTI-PATRONES (nunca hacer)
|
|
|
|
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 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
|