diff --git a/.claude/commands/frontend.md b/.claude/commands/frontend.md index 2678b3a6..d9ad9df0 100644 --- a/.claude/commands/frontend.md +++ b/.claude/commands/frontend.md @@ -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 ( - - {/* Router y contenido aqui */} - - ) -} +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( + + + + + + , +) ``` ### 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: `` para acciones, `` para navegacion, `` para modales + - Elementos semanticos: `` para acciones, `` para navegacion - NUNCA `` 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 { - // props adicionales con JSDoc - /** Descripcion de la prop */ +// Re-export con defaults o logica adicional si necesario +interface MySelectProps extends Omit { customProp?: string } -const Component = React.forwardRef( - ({ className, variant, size, customProp, ...props }, ref) => { - return ( - - ) - } -) -Component.displayName = 'Component' +function MySelect({ customProp, ...props }: MySelectProps) { + return +} -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 ( }> + }> } /> @@ -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. **``** → usar `` 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. **``** → usar `` 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 diff --git a/.claude/rules/frontend_theming.md b/.claude/rules/frontend_theming.md index cafe0434..9dbb2fb3 100644 --- a/.claude/rules/frontend_theming.md +++ b/.claude/rules/frontend_theming.md @@ -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`. diff --git a/frontend/components.json b/frontend/components.json deleted file mode 100644 index 6167d26f..00000000 --- a/frontend/components.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "base-nova", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "rtl": false, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "menuColor": "default", - "menuAccent": "subtle", - "registries": {} -} diff --git a/frontend/functions/core/chart_colors.ts b/frontend/functions/core/chart_colors.ts index fc55e8cf..062443b2 100644 --- a/frontend/functions/core/chart_colors.ts +++ b/frontend/functions/core/chart_colors.ts @@ -1,11 +1,11 @@ export const chartColors = [ - 'hsl(var(--chart-1, 220 70% 50%))', - 'hsl(var(--chart-2, 160 60% 45%))', - 'hsl(var(--chart-3, 30 80% 55%))', - 'hsl(var(--chart-4, 280 65% 60%))', - 'hsl(var(--chart-5, 340 75% 55%))', + '#3b82f6', + '#10b981', + '#f59e0b', + '#8b5cf6', + '#ef4444', ] export function getChartColor(index: number): string { - return chartColors[index % chartColors.length] + return chartColors[index % chartColors.length]! } diff --git a/frontend/functions/core/cn.md b/frontend/functions/core/cn.md deleted file mode 100644 index f36db57f..00000000 --- a/frontend/functions/core/cn.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: cn -kind: function -lang: ts -domain: core -version: "1.0.0" -purity: pure -signature: "cn(...inputs: ClassValue[]): string" -description: "Combina clases CSS con clsx y resuelve conflictos Tailwind con tailwind-merge. Utilidad fundamental para composición de estilos." -tags: [css, tailwind, classname, merge, utility] -uses_functions: [] -uses_types: [] -returns: [] -returns_optional: false -error_type: "" -imports: [clsx, tailwind-merge] -params: - - name: inputs - desc: "Clases CSS en cualquier formato: strings, arrays, objetos con condiciones booleanas" -output: "String con clases CSS combinadas y mergeadas, sin duplicados y conflictos Tailwind resueltos" -tested: false -tests: [] -test_file_path: "" -file_path: "frontend/functions/core/cn.ts" -source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" -source_license: "MIT" -source_file: "frontend/src/lib/utils.ts" ---- - -## Ejemplo - -```typescript -cn("px-4 py-2", "px-6") // "px-6 py-2" (tailwind-merge resuelve conflicto) -cn("text-red-500", false && "hidden") // "text-red-500" (clsx filtra falsy) -cn("rounded-lg", className) // composición con className externo -``` - -## Notas - -Base de todo el sistema de estilos. Todos los componentes la usan para componer className. diff --git a/frontend/functions/core/cn.ts b/frontend/functions/core/cn.ts deleted file mode 100644 index e3155433..00000000 --- a/frontend/functions/core/cn.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]): string { - return twMerge(clsx(inputs)) -} diff --git a/frontend/functions/core/generate_theme_css.md b/frontend/functions/core/generate_theme_css.md deleted file mode 100644 index 7b971d57..00000000 --- a/frontend/functions/core/generate_theme_css.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: generate_theme_css -kind: function -lang: ts -domain: core -version: "1.0.0" -purity: pure -signature: "generateThemeCss(colors: Record, selector?: string): string" -description: "Genera un bloque CSS con variables de tema a partir de un objeto de tokens. Convierte claves camelCase a kebab-case automaticamente. Pura — solo transforma datos, no accede al DOM." -tags: [theme, css, generator, pure] -uses_functions: [] -uses_types: [] -returns: [] -returns_optional: false -error_type: "" -imports: [] -params: - - name: colors - desc: "Objeto con pares clave-valor de nombre variable CSS a valor de color" - - name: selector - desc: "Selector CSS donde inyectar variables (':root' por defecto)" -output: "String con bloque CSS completo conteniendo definiciones de variables de tema" -tested: false -tests: [] -test_file_path: "" -file_path: "frontend/functions/core/generate_theme_css.ts" ---- - -## Ejemplo - -```typescript -import { generateThemeCss } from './generate_theme_css' -import { themeConfigToColors } from './theme_config_to_colors' -import { darkTheme } from '../ui/themes' - -// Generar CSS para inyectar en