Files
fn_registry/.claude/commands/frontend.md
T
egutierrez be71b15afd chore: regla frontend_theming y comandos claude
Nueva regla para usar componentes @fn_library y sistema de temas CSS variables en todos los frontends. Añade directorio de comandos claude.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 03:23:52 +02:00

17 KiB

/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.


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:

# 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:

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
    index.html
    src/
      main.tsx              # Entry point
      App.tsx               # Root con ThemeProvider + Router
      app.css               # Tokens CSS — NUNCA hardcodear colores
      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

{
  "name": "{nombre}",
  "private": true,
  "version": "0.0.1",
  "type": "module",
  "scripts": {
    "dev": "vite --host",
    "build": "tsc -b && vite build",
    "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",
    "react": "^19.2.4",
    "react-dom": "^19.2.4",
    "recharts": "^2.15.0",
    "tailwind-merge": "^3.5.0"
  },
  "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",
    "typescript": "~5.9.3",
    "vite": "^8.0.0"
  }
}

Agregar dependencias extras segun necesidad:

  • Tablas: @tanstack/react-table
  • Charts: recharts
  • Iconos extra: @phosphor-icons/react
  • Forms: react-hook-form, @hookform/resolvers, zod
  • 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

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()],
  resolve: {
    alias: {
      '@': resolve(__dirname, './src'),
      '@fn_library': resolve(__dirname, '../../../frontend/functions/ui'),
    },
    dedupe: ['react', 'react-dom'],
  },
  build: {
    target: 'es2022',
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
        },
      },
    },
  },
})

app.css base

@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;
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

App.tsx base

import { ThemeProvider } from '@fn_library'

export default function App() {
  return (
    <ThemeProvider defaultTheme="dark">
      {/* Router y contenido aqui */}
    </ThemeProvider>
  )
}

Despues del scaffold

cd apps/{nombre}/frontend && pnpm install

CREAR COMPONENTE

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
  6. Accesibilidad:
    • Elementos semanticos: <button> para acciones, <a> para navegacion, <dialog> para modales
    • NUNCA <div onClick> para elementos interactivos
    • aria-label o aria-labelledby en todo componente interactivo
    • 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:
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

import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../core/cn'

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 */
  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'

export { Component, componentVariants }
export type { ComponentProps }

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}.

---
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: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", "class-variance-authority"]
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"
  - name: className
    type: "string"
    required: false
    description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: [default, secondary]
---

## Ejemplo

...codigo de ejemplo...

## Notas

...notas relevantes...

Despues de crear

./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)

// 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):

// 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):

// 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):

// Usar hooks del registry
import { useWailsQuery, useWailsMutation } from '@fn_library'

function useFeatureData() {
  return useWailsQuery('GetFeatureData', [], { staleTime: 60_000 })
}

Code splitting por ruta

import { lazy, Suspense } from 'react'
import { Skeleton } from '@fn_library'

const FeaturePage = lazy(() => import('./features/feature/components/FeaturePage'))

function AppRoutes() {
  return (
    <Routes>
      <Route path="/feature" element={
        <Suspense fallback={<Skeleton className="h-screen w-full" />}>
          <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 (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)

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)

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, dialog — 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 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'
  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()

$ARGUMENTS