commit 5a824c2eee5b3ffa8a320f3a439e2f28032942a3 Author: Egutierrez Date: Tue Apr 21 19:06:49 2026 +0200 initial: mirror of @fn_library from fn_registry 75 components + DESIGN_SYSTEM.md + sync script. Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d58f40d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.DS_Store +*.log +.env +.env.* +!.env.example diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md new file mode 100644 index 0000000..84d115b --- /dev/null +++ b/DESIGN_SYSTEM.md @@ -0,0 +1,457 @@ +# @fn_library — Design System + +> **Fuente de verdad para Claude Design y para cualquier agente que genere UI en este repo.** +> Si generas código frontend, obedece este documento. Sin excepciones. + +--- + +## 1. Qué es esto + +`@fn_library` es el design system del registry. Son 75 wrappers React sobre **Mantine v9** que viven en `frontend/functions/ui/`. Toda app del registry (`apps/*/frontend/`) los consume vía el alias Vite `@fn_library` — barrel único en `frontend/functions/ui/index.ts`. + +**Regla suprema:** en las apps NUNCA se importa Mantine directamente, NUNCA se mezclan otras librerías de UI, NUNCA se escribe CSS manual. Todo pasa por `@fn_library`. + +--- + +## 2. Stack + +| Capa | Librería | Notas | +|------|----------|-------| +| Framework | React 19 | con TypeScript 6 | +| UI base | `@mantine/core` v9 | todos los wrappers la usan | +| Charts | `@mantine/charts` v9 | line, bar, area, pie — Recharts por debajo | +| Fechas | `@mantine/dates` v9 + `dayjs` | DatePickerInput | +| Forms | `@mantine/form` v9 | validación, no es obligatorio | +| Dropzone | `@mantine/dropzone` v9 | drag & drop de ficheros | +| Hooks | `@mantine/hooks` v9 | disclosure, useForm, etc. | +| Notifs | `@mantine/notifications` v9 | incluidas en FnMantineProvider | +| Iconos | `@tabler/icons-react` v3 | el set nativo de Mantine | +| Fuente | `@fontsource-variable/geist` | cargada en el provider | +| Grafos | `sigma` + `graphology` | solo para `GraphContainer` | +| Build | Vite 8 + pnpm | no webpack, no npm, no yarn | + +### Deny-list (NO usar nunca) + +- ❌ `tailwindcss`, `postcss-preset-tailwind` +- ❌ `class-variance-authority` (CVA), `clsx`, `cn` +- ❌ `shadcn/ui`, `@radix-ui/*` directos, `@headlessui/*` +- ❌ `lucide-react`, `heroicons`, `feather`, `phosphor-icons` +- ❌ `chakra-ui`, `mui`, `antd` +- ❌ `framer-motion` (Mantine trae transiciones suficientes) +- ❌ `styled-components`, `emotion` +- ❌ CSS modules, CSS manual (`.css` inline), `style={{...}}` para maquetar + +Si una idea solo encaja con algo de la deny-list → pregunta antes de escribir código. + +--- + +## 3. Import rules + +### 3.1 Un solo barrel + +```ts +// ✅ Correcto — siempre desde el barrel +import { Button, Card, CardHeader, CardTitle, KPICard, LineChart, DataTable } from '@fn_library' + +// ❌ Prohibido — subpath +import { Button } from '@fn_library/button' + +// ❌ Prohibido — Mantine directo en apps +import { Button } from '@mantine/core' +``` + +### 3.2 Provider raíz obligatorio + +Toda app monta su árbol dentro de `FnMantineProvider`. No `MantineProvider` directo. + +```tsx +import { FnMantineProvider } from '@fn_library' +import { createTheme } from '@mantine/core' + +const theme = createTheme({ + primaryColor: 'indigo', + fontFamily: 'Geist Variable, sans-serif', +}) + +export function App() { + return ( + + + + ) +} +``` + +`FnMantineProvider` ya incluye: +- `` +- Los CSS de `@mantine/core`, `@mantine/charts`, `@mantine/notifications` +- `defaultColorScheme="dark"` por defecto + +### 3.3 Iconos + +```tsx +import { IconPlus, IconTrash, IconRefresh, IconSearch } from '@tabler/icons-react' + + +``` + +### 3.4 Layout: solo componentes Mantine de layout + +Los componentes de layout (`AppShell`, `Group`, `Stack`, `Grid`, `SimpleGrid`, `Flex`, `Container`, `Box`, `Paper`) **sí** se importan directo de `@mantine/core`, porque `@fn_library` no los wrappea — son parte fija del sistema. + +```tsx +import { Stack, Group, SimpleGrid, Paper, Box } from '@mantine/core' +``` + +El único layout wrappeado es `FnAppShell` (en `@fn_library`) — úsalo para el shell principal de la app. + +--- + +## 4. Reglas duras (obedecer siempre) + +1. **Props Mantine, no clases.** Tamaños, márgenes, padding, gap, radio, color: `size`, `p`, `m`, `mt`, `mb`, `gap`, `radius`, `shadow`, `c`, `fw`, `fz`, `lh`. Sin `className="..."` con clases propias. +2. **Color scheme dark** es el default del registro. Los componentes están pensados para oscuro; si quieres claro, pásalo explícito. +3. **No hardcodees colores**. Usa los tokens Mantine (`--mantine-color-*`) o los helpers del registry (`get_series_color_ts_core`, `chart_colors_ts_core`). +4. **Formulario**: envuelve cada campo con ``. Gestiona ARIA, label, error. +5. **Tablas**: usa `DataTable`. Auto-detecta columnas del primer row. No escribas `` a mano. +6. **KPIs**: usa `KPICard` (no Paper manual). Acepta `label`, `value`, `unit`, `delta`, `icon`, `chart`, `action`, `subtitle`. +7. **Charts**: siempre con `@fn_library` charts; nunca Recharts directo. Envuelve en `ChartContainer` si necesitas header/footer del propio chart. +8. **Loading**: `Skeleton` + variantes (`SkeletonText`, `SkeletonCard`, `SkeletonTable`, `SkeletonAvatar`, `SkeletonButton`) o `FnLoadingOverlay` para secciones completas. +9. **Vacío**: `EmptyState` con icono Tabler, título, descripción, acción opcional. +10. **Errores de página entera**: `ErrorPage`. 404/500/403 o código custom. +11. **Modal**: `Dialog`. Drawer lateral: `Sheet`. Popover flotante: `Popover`. Menús: `DropdownMenu`. +12. **Notificaciones**: desde `@mantine/notifications` `notifications.show({...})` — el provider ya está montado. O usa el `Toast` del registry si necesitas la API `useToast`. + +--- + +## 5. Catálogo completo (75 componentes) + +> Todos exportados desde `@fn_library` (barrel). Cada uno detalla variantes y nota clave. + +### 5.1 Primitives + +| Componente | Variantes | Notas | +|------------|-----------|-------| +| `Button` | `default \| outline \| secondary \| ghost \| destructive \| link` — sizes `default \| xs \| sm \| lg \| icon \| icon-xs \| icon-sm \| icon-lg` | Wrapper Mantine Button. Soporta `leftSection`/`rightSection` con icono. | +| `FnActionIcon` | `filled \| light \| outline \| transparent \| default \| subtle` | Botón cuadrado para icono único. Loading + tooltip integrados. | +| `Badge` | 10 semánticas (`default, secondary, destructive, outline, ghost, link, success, warning, error, info`) | 2 tamaños. | +| `Chip` | `filled \| outline \| light` | Seleccionable. `ChipGroup` para selección simple/múltiple. | +| `Indicator` (FnIndicator) | — | Badge posicionado sobre un hijo. | +| `Label` | — | Etiqueta de formulario accesible. | +| `Skeleton` | `base \| text \| card \| avatar \| button \| table` | Exporta también `SkeletonText`, `SkeletonCard`, `SkeletonTable`, `SkeletonAvatar`, `SkeletonButton`. | +| `Sparkline` | `line \| area \| bar` | Mini chart SVG puro — para tablas y KPIs. | +| `ProgressBar` | `primary \| success \| warning \| destructive` | Con buffer, indeterminado y display de valor. | +| `FnRingProgress` | — | Anillo de progreso con secciones. | +| `Tooltip` | `default` | Delay configurable. | +| `Avatar` | `xs \| sm \| md \| lg \| xl` | Fallback a iniciales. | + +### 5.2 Inputs + +| Componente | Notas | +|------------|-------| +| `Input` | `TextInput` con `InputGroup` e `InputIcon`. | +| `Textarea` | autosize. | +| `PasswordInput` | Toggle visibilidad + strength meter opcional. | +| `NumberInput` (FnNumberInput) | min/max, step, prefix, suffix. | +| `Select` | Declarativo `data={[...]}`. Búsqueda, grupos. | +| `SimpleSelect` | Acepta array plano o agrupado. | +| `MultiSelect` | Pills + límite. | +| `Autocomplete` | Admite valores libres (a diferencia de Select). | +| `TagsInput` | Tags libres. | +| `DatePickerInput` | Fecha simple, múltiple o rango. | +| `FileInput` | Ficheros único/múltiple, tipos MIME. | +| `Dropzone` | Drag & drop de archivos. Estados idle/accept/reject. | +| `Checkbox` | Con indeterminate. | +| `RadioGroup` | Opciones exclusivas. | +| `SwitchToggle` | Label a izquierda o derecha. | +| `FnSegmentedControl` | Selección única entre opciones. | +| `Slider` / `RangeSlider` | Marcas, labels. | +| `Rating` | Fracciones, símbolos custom. | +| `ColorInput` | Picker + swatches + alpha. | +| `PinInput` | Código OTP con autocompletado. | +| `SearchBar` | Input con debounce, icono y clear. | +| `Command` / `CommandSearch` | Combobox cmdk-style con búsqueda, grupos, shortcuts. | +| `FormField` | **Wrapper obligatorio** para cada campo de form. | + +### 5.3 Data display + +| Componente | Notas | +|------------|-------| +| `DataTable` | Sticky header, overflow scroll, heatmap, formato condicional (number/datetime/currency), auto-columns. Variantes: `default \| heatmap`. | +| `KPICard` | Size `sm \| default \| lg`. Acepta icon, chart inline, action. | +| `Card` + slots | `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`. Variantes `default \| borderless \| ghost`, sizes `default \| sm`. | +| `FnTimeline` | Items con iconos y colores. | +| `EmptyState` | Icono + título + descripción + acción. | +| `Pagination` | Autocontenido. | + +### 5.4 Charts + +| Componente | Variantes | Notas | +|------------|-----------|-------| +| `LineChart` | 5 curvas (`linear, monotone, step, stepBefore, stepAfter`) | Multi-series, reference lines. | +| `BarChart` | `vertical \| horizontal` | Multi-series. | +| `AreaChart` | `default \| stacked` | Gradientes auto. | +| `PieChart` | `pie \| donut` | Colores auto. | +| `Sparkline` | `line \| area \| bar` | Mini chart inline sin Recharts. | +| `ChartContainer` | `default` | Wrapper Paper + helpers (`getSeriesColor`, `Series`). | +| `GraphContainer` | — | sigma.js + graphology + ForceAtlas2. | + +### 5.5 Overlays y feedback + +| Componente | Notas | +|------------|-------| +| `Dialog` | Modal con slots (`DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogContent`, `DialogFooter`, `DialogClose`). | +| `Sheet` | Drawer `top \| bottom \| left \| right`. | +| `Popover` | Contenido flotante. | +| `DropdownMenu` | Items, checkboxes, radios, separadores, submenús. | +| `Alert` | `default \| destructive`. Slots `AlertTitle`, `AlertDescription`. | +| `Toast` | `default \| success \| error \| warning \| info`. API `useToast`. | +| `FnLoadingOverlay` | Blur + opacidad. Para secciones o página entera. | + +### 5.6 Navegación y shell + +| Componente | Notas | +|------------|-------| +| `FnAppShell` | Shell con header + navbar colapsable + main. | +| `PageHeader` | `full \| simple`. Título, subtítulo, acciones, back, tabs integrados, badge, sticky. | +| `Breadcrumb` | Con elipsis y router links vía `asChild`. | +| `FnNavLink` | Link con icono, descripción, anidamiento. | +| `Tabs` | `default \| line`. Horizontal/vertical. | +| `Accordion` | `AccordionItem` + `AccordionTrigger` + `AccordionContent`. | +| `FnStepper` | Horizontal/vertical. | + +### 5.7 Hooks y providers (TS) + +| Hook / Provider | Fuente | Notas | +|---|---|---| +| `FnMantineProvider` | `@fn_library` | Provider raíz. | +| `useToast` | `@fn_library` (toast) | API imperativa para notificaciones. | +| `useAnimatedCanvas` | `@fn_library` | Canvas con requestAnimationFrame, DPR, FPS real. | +| `WailsProvider` + `useWailsContext` + `useWailsCache` | `@fn_library` | Solo si la app es Wails. | +| `useWailsQuery` / `useWailsMutation` / `useWailsStream` / `useWailsEvent` | `@fn_library` | IPC Wails. | + +### 5.8 Hooks TS core (registry TS core, no UI) + +No están en `@fn_library` pero son parte del stack. Se importan desde `frontend/functions/core/` o se copian al proyecto según se necesite: + +- `use_fetch`, `use_mutation`, `use_sse`, `use_websocket`, `use_infinite_scroll`, `use_debounced_search`, `use_form` +- `api_client`, `http_cache`, `format_compact` (K/M/B, bytes, duración, Hz), `chart_colors`, `get_series_color`, `get_computed_color` + +--- + +## 6. Page generators (preferir antes que montar a mano) + +Cuando generes una pantalla, intenta **primero** con un page generator. Solo si no encaja, compone con Card/Paper/SimpleGrid. + +| Generator | Cuándo usarlo | Props clave | +|-----------|---------------|-------------| +| `analyticsPage` | Dashboard con KPIs + charts en grid | `title, subtitle, metrics[], charts[], dateRange?, actions?` | +| `crudPage` | Listado + tabla + modal create/edit/delete | `title, columns, rows, schema, onAdd, onEdit, onDelete` | +| `dashboardLayout` | Grid responsive 1-4 cols de widgets con span | `widgets[{content, span}]` | +| `detailPage` | Ficha de entidad con header, campos, tabs, timeline | `entity, fields[], tabs[], timeline[]` | +| `settingsPage` | Config con navegación lateral y secciones | `sections[{nav, fields[]}]` | +| `AuthForm` | Login / register con social buttons | `mode, onSubmit, extraFields?, socialProviders?` | +| `ErrorPage` | 404 / 500 / 403 / custom | `code, title, description, actions` | + +Estas funciones viven en `frontend/functions/ui/` igual que los componentes, se importan desde `@fn_library` y retornan un `ReactElement`. + +--- + +## 7. Ejemplos canónicos + +### 7.1 Dashboard de analytics + +```tsx +import { analyticsPage, LineChart, BarChart, AreaChart, PieChart, Button } from '@fn_library' +import { IconRefresh } from '@tabler/icons-react' + +const runsData = [ + { hour: '00', success: 120, failure: 3 }, + { hour: '01', success: 115, failure: 1 }, + // ... +] + +export const PipelineMonitoring = () => analyticsPage({ + title: 'Pipeline Monitoring', + subtitle: 'últimas 24h', + actions: , + metrics: [ + { label: 'Runs totales', value: '1,284', delta: { value: 12, isPositive: true } }, + { label: 'Success rate', value: '97.3%', delta: { value: 0.4, isPositive: true } }, + { label: 'P95 latency', value: '842ms', delta: { value: -8, isPositive: true } }, + { label: 'Errores', value: 34, delta: { value: 5, isPositive: false } }, + ], + charts: [ + { id: 'runs', title: 'Runs por hora', type: 'line', + content: }, + { id: 'top', title: 'Top pipelines', type: 'bar', + content: }, + { id: 'latency', title: 'Latencia por etapa', type: 'area', span: 2, + content: }, + ], +}) +``` + +### 7.2 CRUD + +```tsx +import { crudPage, Badge } from '@fn_library' +import type { ColumnDef } from '@fn_library' + +interface Target { app: string; host: string; port: number; status: 'ok' | 'down' } + +const columns: ColumnDef[] = [ + { key: 'app', label: 'App' }, + { key: 'host', label: 'Host', render: (v) => {v as string} }, + { key: 'port', label: 'Port', format: 'number' }, + { key: 'status', label: 'Status', + render: (v) => {v as string} }, +] + +export const DeployTargets = () => crudPage({ + title: 'Deploy Targets', + columns, + rows: [], + schema: { + app: { type: 'text', label: 'App', required: true }, + host: { type: 'select', label: 'Host', options: ['vps-1','vps-2'] }, + port: { type: 'number', label: 'Port', min: 1, max: 65535 }, + build: { type: 'textarea', label: 'Build cmd' }, + }, + onAdd: async (row) => { /* ... */ }, + onEdit: async (row) => { /* ... */ }, + onDelete: async (row) => { /* ... */ }, +}) +``` + +### 7.3 Página de detalle + +```tsx +import { detailPage, Badge } from '@fn_library' + +export const FunctionDetail = (fn) => detailPage({ + entity: { title: fn.id, subtitle: fn.description, avatar: fn.name.slice(0,2).toUpperCase(), + badge: {fn.domain}, backHref: '/functions' }, + fields: [ + { label: 'Language', value: fn.lang }, + { label: 'Purity', value: fn.purity }, + { label: 'Kind', value: fn.kind }, + { label: 'Version', value: fn.version }, + { label: 'Tested', value: fn.tested ? 'yes' : 'no' }, + { label: 'Source', value: fn.source_repo || '—' }, + ], + tabs: [ + { key: 'code', label: 'Code', count: 1, content:
{fn.code}
}, + { key: 'tests', label: 'Tests', count: fn.tests_count }, + { key: 'deps', label: 'Deps', count: fn.deps_count }, + ], + timeline: fn.activity, +}) +``` + +--- + +## 8. Theming + +Cada app define su `theme`: + +```tsx +import { createTheme, type MantineThemeOverride } from '@mantine/core' + +export const theme: MantineThemeOverride = createTheme({ + primaryColor: 'indigo', + primaryShade: { light: 6, dark: 4 }, + fontFamily: 'Geist Variable, sans-serif', + headings: { fontFamily: 'Geist Variable, sans-serif', fontWeight: '600' }, + defaultRadius: 'md', + // colores custom solo si son parte del brand + colors: { + brand: ['#f0f4ff','#d9e2ff','#b3c7ff','#8daaff','#668dff','#3f70ff','#3059d9','#2242b3','#13298d','#041066'], + }, +}) +``` + +Y lo pasa al provider: + +```tsx + + + +``` + +**No** definas variables CSS custom, **no** toques `:root`, **no** cargues hojas de estilo adicionales. Mantine genera todas las variables (`--mantine-color-*`, `--mantine-spacing-*`, `--mantine-radius-*`) automáticamente del `theme`. + +--- + +## 9. Anti-patrones (rechazar en code review) + +```tsx +// ❌ className con clases custom + + +// ❌ CSS modules, Tailwind, style inline +
+
+
+// ✅ en su lugar + + +// ❌ Mantine directo en apps +import { Button } from '@mantine/core' +// ✅ +import { Button } from '@fn_library' + +// ❌ Iconos ajenos +import { Plus } from 'lucide-react' +// ✅ +import { IconPlus } from '@tabler/icons-react' + +// ❌ HTML nativo para formulario + +// ✅ + + +// ❌
a mano +
......
+// ✅ + + +// ❌ Recharts directo +import { LineChart } from 'recharts' +// ✅ +import { LineChart } from '@fn_library' +``` + +--- + +## 10. Checklist para handoffs de Claude Design + +Antes de integrar un export al repo, verificar: + +- [ ] Todos los imports UI vienen de `@fn_library` (excepto layout Mantine: `Stack`, `Group`, `SimpleGrid`, `Grid`, `Paper`, `Box`, `Container`, `Flex`, `Title`, `Text`, `AppShell`). +- [ ] Cero imports de `lucide-react`, `shadcn`, `@radix-ui/*`, `tailwindcss`, `clsx`, `cn`, `cva`, `framer-motion`, `chakra`, `mui`, `antd`. +- [ ] Cero `className` con clases CSS propias (solo se toleran las que el wrapper pasa al nodo raíz para forwardref). +- [ ] Iconos únicamente `@tabler/icons-react`. +- [ ] Charts únicamente `LineChart`/`BarChart`/`AreaChart`/`PieChart`/`Sparkline` del registry. +- [ ] Formulario con `FormField` wrapper para cada campo. +- [ ] Tablas con `DataTable`. +- [ ] KPIs con `KPICard`. +- [ ] Página entera envuelta en `FnMantineProvider` (solo el root). +- [ ] Si aparece un componente que no existe en `@fn_library`, decidir: (a) reescribirlo con wrappers existentes, o (b) extraerlo como función nueva al registry (`frontend/functions/ui/{name}.tsx` + `.md` + `fn index`). + +--- + +## 11. Metadata para Claude Design + +Cuando enlaces este repo en `claude.ai/design`, apunta al subdirectorio **`frontend/`** (no a la raíz del monorepo). Lo único que necesita leer para entender el sistema: + +``` +frontend/ + DESIGN_SYSTEM.md ← este documento (regla suprema) + package.json ← dependencias runtime + functions/ui/ ← los 75 componentes con .tsx + .md + index.ts ← barrel @fn_library +``` + +Si aun así es demasiado, usa el **repo espejo** `fn-design-system` (generado desde este repo) que contiene exactamente esos archivos y nada más. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bafd62d --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# fn-design-system + +**Read-only mirror of `@fn_library`** — the React 19 + Mantine v9 design system that lives inside [fn_registry](../fn_registry). + +This repo exists for one purpose: **give Claude Design (and other external tools) a clean, minimal view of the design system** without exposing the rest of the monorepo. + +> ⚠️ Do not edit files in `components/` directly. They are overwritten on each sync from `fn_registry`. + +## Structure + +``` +fn-design-system/ + DESIGN_SYSTEM.md ← the contract (read this first) + components/ ← mirror of fn_registry/frontend/functions/ui/ + index.ts ← @fn_library barrel + *.tsx + *.md ← 75 components, one pair each + package.json ← runtime deps (Mantine v9, Tabler, React 19) + tsconfig.json ← paths → @fn_library maps to ./components + sync_from_registry.sh ← re-syncs from fn_registry +``` + +## How to use + +### For Claude Design + +1. Link this repo to your Claude Design project (`claude.ai/design` → Settings → Connected repos). +2. In prompts, refer to components **by name from `@fn_library`** and obey `DESIGN_SYSTEM.md`. +3. Handoff to Claude Code → the generated code lands in your local `fn_registry` apps. + +### For humans / agents + +Read `DESIGN_SYSTEM.md`. It is the single source of truth. + +## How to re-sync + +When you add or modify components in `fn_registry/frontend/functions/ui/`: + +```bash +./sync_from_registry.sh +git add -A +git commit -m "sync: " +git push +``` + +The script requires `FN_REGISTRY_ROOT` to point at your local fn_registry clone (defaults to `~/fn_registry`). + +## What is NOT in this repo + +- Any application code (`apps/*/`) +- The registry itself (`registry.db`, `cmd/fn/`, Go code) +- Python / Bash functions +- Deploy config, operations DBs, secrets + +Everything here is derivable from `fn_registry` — if this repo is lost, `./sync_from_registry.sh` rebuilds it. diff --git a/components/accordion.md b/components/accordion.md new file mode 100644 index 0000000..41d611f --- /dev/null +++ b/components/accordion.md @@ -0,0 +1,54 @@ +--- +name: accordion +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Accordion(props: AccordionProps): JSX.Element" +description: "Secciones colapsables con animaciones. Mantine Accordion. Composable: AccordionItem + AccordionTrigger + AccordionContent." +tags: [accordion, collapsible, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Accordion que renderiza secciones colapsables con soporte para múltiples items abiertos simultáneamente" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/accordion.tsx" +props: + - name: className + type: "string" + required: false + description: "Clases CSS adicionales para el contenedor" +emits: [] +has_state: false +framework: react +variant: [] +--- + +## Ejemplo + +```tsx + + + Seccion 1 + + Contenido de la primera seccion. + + + + Seccion 2 + + Contenido de la segunda seccion. + + + +``` + +## Notas + +Usa Mantine Accordion nativo. Soporta type single (default) y multiple para multiples items abiertos. El chevron se maneja automaticamente por Mantine. AccordionItem requiere prop value unico. Exports: Accordion, AccordionItem, AccordionTrigger, AccordionContent. diff --git a/components/accordion.tsx b/components/accordion.tsx new file mode 100644 index 0000000..2d9c193 --- /dev/null +++ b/components/accordion.tsx @@ -0,0 +1,90 @@ +import * as React from "react" +import { Accordion as MantineAccordion } from "@mantine/core" + +interface AccordionItem { + value: string + trigger: React.ReactNode + content: React.ReactNode + disabled?: boolean +} + +interface AccordionProps { + items?: AccordionItem[] + type?: "single" | "multiple" + defaultValue?: string | string[] + className?: string + itemClassName?: string + children?: React.ReactNode +} + +function Accordion({ className, type, defaultValue, children }: AccordionProps) { + if (type === "multiple") { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +} + +interface AccordionItemProps { + value: string + className?: string + children?: React.ReactNode + disabled?: boolean +} + +function AccordionItem({ className, value, children, ...props }: AccordionItemProps) { + return ( + + {children} + + ) +} + +function AccordionTrigger({ className, children, ...props }: { className?: string; children?: React.ReactNode }) { + return ( + + {children} + + ) +} + +function AccordionContent({ className, children, ...props }: { className?: string; children?: React.ReactNode }) { + return ( + + {children} + + ) +} + +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } +export type { AccordionItem as AccordionItemData, AccordionProps } diff --git a/components/action_icon.md b/components/action_icon.md new file mode 100644 index 0000000..61f4c34 --- /dev/null +++ b/components/action_icon.md @@ -0,0 +1,77 @@ +--- +name: action_icon +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnActionIcon(props: FnActionIconProps): JSX.Element" +description: "Boton de icono con variantes, loading y tooltip opcional. Wrapper sobre Mantine ActionIcon." +tags: [mantine, button, icon, action, tooltip, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: icon + type: "ReactNode" + required: true + description: "Icono a renderizar dentro del boton" + - name: variant + type: "'filled' | 'light' | 'outline' | 'transparent' | 'default' | 'subtle'" + required: false + description: "Variante visual del boton, default 'default'" + - name: size + type: "MantineSize | number" + required: false + description: "Tamano del boton" + - name: color + type: "MantineColor" + required: false + description: "Color del boton" + - name: onClick + type: "MouseEventHandler" + required: false + description: "Callback al hacer click" + - name: loading + type: "boolean" + required: false + description: "Muestra spinner de carga" + - name: disabled + type: "boolean" + required: false + description: "Deshabilita el boton" + - name: tooltip + type: "string" + required: false + description: "Si se provee, envuelve el boton en un Tooltip" +output: "Boton de icono con tooltip opcional, estados loading/disabled y multiples variantes" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/action_icon.tsx" +emits: [] +has_state: false +variant: [filled, light, outline, transparent, default, subtle] +--- + +## Ejemplo + +```tsx +import { FnActionIcon } from '@fn_library' +import { IconSettings } from '@tabler/icons-react' + +} + tooltip="Configuracion" + variant="light" + onClick={() => openSettings()} +/> +``` + +## Notas + +Wrapper sobre Mantine `ActionIcon`. Si se provee `tooltip`, envuelve automaticamente en Mantine `Tooltip`. Compatible con iconos de `@tabler/icons-react`. diff --git a/components/action_icon.tsx b/components/action_icon.tsx new file mode 100644 index 0000000..20b6fe3 --- /dev/null +++ b/components/action_icon.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { ActionIcon, Tooltip } from '@mantine/core' +import type { MantineSize, MantineColor } from '@mantine/core' + +interface FnActionIconProps { + icon: React.ReactNode + variant?: 'filled' | 'light' | 'outline' | 'transparent' | 'default' | 'subtle' + size?: MantineSize | number + color?: MantineColor + onClick?: React.MouseEventHandler + loading?: boolean + disabled?: boolean + tooltip?: string +} + +function FnActionIcon({ + icon, + variant = 'default', + size = 'md', + color, + onClick, + loading, + disabled, + tooltip, +}: FnActionIconProps) { + const button = ( + + {icon} + + ) + + if (tooltip) { + return {button} + } + + return button +} + +export { FnActionIcon } +export type { FnActionIconProps } diff --git a/components/alert.md b/components/alert.md new file mode 100644 index 0000000..22917d5 --- /dev/null +++ b/components/alert.md @@ -0,0 +1,50 @@ +--- +name: alert +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Alert(props: { variant?: 'default' | 'destructive' }): JSX.Element" +description: "Alerta accesible con variantes default y destructive. Mantine Alert con slots para título, descripción y acción." +tags: [alert, feedback, component, ui, notification, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", react] +output: "Componente Alert que renderiza una alerta accesible via Mantine Alert con slots para título, descripción y acción" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/alert.tsx" +props: + - name: variant + type: "'default' | 'destructive'" + required: false + description: "Variante visual" +emits: [] +has_state: false +framework: react +variant: [default, destructive] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/alert.tsx" +--- + +## Ejemplo + +```tsx + + + Error + Something went wrong. + +``` + +## Notas + +Exporta 4 subcomponentes composables via data-slot: Alert, AlertTitle, AlertDescription, AlertAction. +AlertAction se posiciona absolute top-right para acciones secundarias (ej: boton cerrar). +alertVariants se exporta como objeto vacio por compatibilidad (Mantine gestiona variantes via color prop). diff --git a/components/alert.tsx b/components/alert.tsx new file mode 100644 index 0000000..da6ae02 --- /dev/null +++ b/components/alert.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' +import { Alert as MantineAlert, Box, Text } from '@mantine/core' + +type AlertVariant = 'default' | 'destructive' + +const variantColorMap: Record = { + default: undefined, + destructive: 'red', +} + +function Alert({ + className, + variant = 'default', + children, + ...props +}: React.ComponentProps<'div'> & { variant?: AlertVariant }) { + return ( + + {children} + + ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +function AlertAction({ className, style, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +const alertVariants = {} as const + +export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants } diff --git a/components/analytics_page.md b/components/analytics_page.md new file mode 100644 index 0000000..ff60a97 --- /dev/null +++ b/components/analytics_page.md @@ -0,0 +1,51 @@ +--- +name: analytics_page +kind: function +lang: ts +domain: ui +version: "1.0.0" +purity: pure +signature: "analyticsPage(props: AnalyticsPageProps): ReactElement" +description: "Genera un dashboard de analytics completo con header, fila de KPIs con deltas y grid de charts configurables." +tags: [analytics, dashboard, kpi, charts, factory, composition, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react, "@mantine/core"] +params: + - name: props + desc: "Configuración del dashboard: título, métricas con deltas, y lista de charts con span" +output: "Componente ReactElement que renderiza un dashboard de analytics completo con header, KPIs y grid de charts" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/analytics_page.tsx" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "" +--- + +## Ejemplo + +```tsx +analyticsPage({ + title: 'Sales Analytics', + metrics: [ + { label: 'Revenue', value: '$124,500', delta: { value: 12.5, isPositive: true } }, + { label: 'Orders', value: '1,234', delta: { value: -3.2, isPositive: false } }, + { label: 'Avg Order', value: '$101', delta: { value: 0, isPositive: true } }, + { label: 'Customers', value: '892' }, + ], + charts: [ + { id: 'revenue', title: 'Revenue Over Time', type: 'area', span: 2, content: }, + { id: 'orders', title: 'Orders by Category', type: 'bar', content: }, + { id: 'trends', title: 'Customer Trends', type: 'line', content: }, + ], +}) +``` + +## Notas + +Layout inteligente: los KPIs se ajustan automáticamente a 2/3/4 columnas según cantidad. Los charts soportan span para ancho completo. diff --git a/components/analytics_page.tsx b/components/analytics_page.tsx new file mode 100644 index 0000000..a688833 --- /dev/null +++ b/components/analytics_page.tsx @@ -0,0 +1,97 @@ +import * as React from 'react' +import { Stack, Group, Title, Text, Paper, SimpleGrid } from '@mantine/core' + +interface MetricConfig { + label: string + value: string | number + delta?: { value: number; isPositive: boolean } + sparklineData?: number[] +} + +interface ChartConfig { + id: string + title: string + type: 'line' | 'bar' | 'area' + span?: 1 | 2 + height?: number + content: React.ReactNode +} + +interface AnalyticsPageProps { + title: string + subtitle?: string + dateRange?: React.ReactNode + metrics: MetricConfig[] + charts: ChartConfig[] + actions?: React.ReactNode + className?: string +} + +export function analyticsPage({ + title, + subtitle, + dateRange, + metrics, + charts, + actions, +}: AnalyticsPageProps): React.ReactElement { + const metricCols = metrics.length <= 2 ? { base: 1, md: 2 } : metrics.length <= 3 ? { base: 1, md: 3 } : { base: 1, md: 2, lg: 4 } + + return ( + + {/* Header */} + + + {title} + {subtitle && {subtitle}} + + + {dateRange} + {actions} + + + + {/* KPI Row */} + + {metrics.map((metric, i) => ( + + {metric.label} + + + {metric.value} + {metric.delta && ( + + {metric.delta.value > 0 ? '+' : ''}{metric.delta.value}% + + )} + + + + ))} + + + {/* Charts Grid */} + + {charts.map((chart) => ( + + {chart.title} + {chart.content} + + ))} + + + ) +} + +export type { AnalyticsPageProps, MetricConfig, ChartConfig } diff --git a/components/app_shell.md b/components/app_shell.md new file mode 100644 index 0000000..343ce91 --- /dev/null +++ b/components/app_shell.md @@ -0,0 +1,65 @@ +--- +name: app_shell +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnAppShell(props: FnAppShellProps): JSX.Element" +description: "Layout shell con header, navbar colapsable y area principal. Wrapper sobre Mantine AppShell." +tags: [mantine, layout, shell, navigation, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: header + type: "ReactNode" + required: false + description: "Contenido del header superior" + - name: navbar + type: "ReactNode" + required: false + description: "Contenido del sidebar de navegacion" + - name: navbarWidth + type: "number" + required: false + description: "Ancho del navbar en px, default 250" + - name: navbarCollapsed + type: "boolean" + required: false + description: "Si el navbar esta colapsado" + - name: children + type: "ReactNode" + required: true + description: "Contenido principal del area main" +output: "Layout de aplicacion con header fijo, sidebar colapsable y area de contenido principal" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/app_shell.tsx" +emits: [] +has_state: false +variant: [] +--- + +## Ejemplo + +```tsx +import { FnAppShell } from '@fn_library' + +Logo} + navbar={} + navbarCollapsed={collapsed} +> + + +``` + +## Notas + +Wrapper sobre Mantine `AppShell`. El header tiene altura fija de 60px. El navbar colapsa tanto en mobile como en desktop cuando `navbarCollapsed` es true. El breakpoint de responsive es `sm`. diff --git a/components/app_shell.tsx b/components/app_shell.tsx new file mode 100644 index 0000000..f71b327 --- /dev/null +++ b/components/app_shell.tsx @@ -0,0 +1,33 @@ +import * as React from 'react' +import { AppShell } from '@mantine/core' + +interface FnAppShellProps { + header?: React.ReactNode + navbar?: React.ReactNode + navbarWidth?: number + navbarCollapsed?: boolean + children: React.ReactNode +} + +function FnAppShell({ + header, + navbar, + navbarWidth = 250, + navbarCollapsed = false, + children, +}: FnAppShellProps) { + return ( + + {header && {header}} + {navbar && {navbar}} + {children} + + ) +} + +export { FnAppShell } +export type { FnAppShellProps } diff --git a/components/area_chart.md b/components/area_chart.md new file mode 100644 index 0000000..4b9cf0c --- /dev/null +++ b/components/area_chart.md @@ -0,0 +1,57 @@ +--- +name: area_chart +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "AreaChart(props: AreaChartProps): JSX.Element" +description: "Gráfico de área @mantine/charts con gradientes automáticos, multi-series, stacking y tooltips." +tags: [chart, area, visualization, mantine, gradient, component, ui] +uses_functions: [chart_container_ts_ui] +uses_types: [ChartSeries_ts_ui] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/charts", "@mantine/core"] +output: "Componente JSX que renderiza un gráfico de área con gradientes, multi-series y tooltips" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/area_chart.tsx" +props: + - name: data + type: "Record[]" + required: true + description: "Array de datos" + - name: xKey + type: "string" + required: true + description: "Key del eje X" + - name: stacked + type: "boolean" + required: false + description: "Apilar áreas" + - name: gradient + type: "GradientConfig | boolean" + required: false + description: "Gradiente (true por defecto)" + - name: series + type: "Series[]" + required: false + description: "Series de datos para multi-series" +emits: [] +has_state: false +framework: react +variant: [default, stacked] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/charts/area-chart.tsx" +--- + +## Ejemplo + +```tsx + + +``` diff --git a/components/area_chart.tsx b/components/area_chart.tsx new file mode 100644 index 0000000..c7adc1b --- /dev/null +++ b/components/area_chart.tsx @@ -0,0 +1,54 @@ +import { AreaChart as MantineAreaChart } from '@mantine/charts' +import { Paper } from '@mantine/core' +import { type Series, getSeriesColor } from './chart_container' + +interface AreaChartProps { + data: Record[] + xKey: string + yKey?: string + series?: Series[] + stacked?: boolean + gradient?: boolean + showGrid?: boolean + showLegend?: boolean + height?: number + xAxisFormatter?: (value: unknown) => string + yAxisFormatter?: (value: unknown) => string + valueFormatter?: (value: number) => string +} + +function AreaChartComponent({ + data, xKey, yKey, series, stacked = false, gradient = true, showGrid = true, + showLegend = false, height = 300, xAxisFormatter, yAxisFormatter, + valueFormatter = (v) => v.toLocaleString(), +}: AreaChartProps) { + const chartSeries = series + ? series.map((s, i) => ({ name: s.key, label: s.name, color: getSeriesColor(i, s.color) })) + : yKey ? [{ name: yKey, label: yKey, color: getSeriesColor(0) }] : [] + + return ( + + + + ) +} + +/** @deprecated Gradient is handled by Mantine's withGradient prop */ +type GradientConfig = { from: string; to: string } + +export const AreaChart = AreaChartComponent +export type { AreaChartProps, GradientConfig } diff --git a/components/auth_form.md b/components/auth_form.md new file mode 100644 index 0000000..5bcca8b --- /dev/null +++ b/components/auth_form.md @@ -0,0 +1,101 @@ +--- +name: auth_form +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "AuthForm(config: AuthFormConfig): ReactElement" +description: "Genera página de autenticación con toggle login/register, social buttons opcionales, campos extra en registro y validación. Basado en Mantine AuthenticationForm." +tags: [auth, login, register, form, page, ui, mantine, toggle] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", "@mantine/hooks"] +has_state: true +framework: react +emits: [onSubmit] +params: + - name: title + desc: "Título principal que aparece en la cabecera del formulario (default: 'Welcome')" + - name: socialButtons + desc: "Lista de botones de login social, cada uno con label, icono opcional y callback onClick" + - name: extraFields + desc: "Campos de texto adicionales que se muestran únicamente en el modo registro (ej: nombre, empresa)" + - name: onSubmit + desc: "Callback invocado al enviar el formulario con type ('login'|'register'), email, password y valores de extraFields" + - name: defaultType + desc: "Modo inicial del formulario: 'login' (default) o 'register'" + - name: paperProps + desc: "Props de Mantine Paper para personalizar el contenedor (shadow, radius, p, etc.)" +output: "Página de autenticación completa con toggle login/register, campos email/password, botones sociales opcionales y campo de términos en registro" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/auth_form.tsx" +source_repo: "" +source_license: "" +source_file: "" +--- + +## Ejemplo + +### Config mínima (solo login) + +```tsx +import { AuthForm } from '@fn_library/auth_form' + +function LoginPage() { + return ( + { + console.log(type, email, password) + }} + /> + ) +} +``` + +### Con social buttons y campos extra en registro + +```tsx +import { AuthForm } from '@fn_library/auth_form' +import { IconBrandGoogle, IconBrandGithub } from '@tabler/icons-react' + +function AuthPage() { + return ( + , onClick: () => signInWithGoogle() }, + { label: 'GitHub', icon: , onClick: () => signInWithGitHub() }, + ]} + extraFields={[ + { name: 'name', label: 'Nombre completo', placeholder: 'Lucas García', required: true }, + { name: 'company', label: 'Empresa', placeholder: 'Acme Corp' }, + ]} + onSubmit={({ type, email, password, name, company }) => { + if (type === 'register') { + registerUser({ email, password, name, company }) + } else { + loginUser({ email, password }) + } + }} + /> + ) +} +``` + +## Notas + +Función con estado interno (useToggle, useState de @mantine/hooks). Gestiona el toggle entre login y register sin prop externa — el padre solo recibe el valor final via onSubmit.type. + +Los `extraFields` solo se renderizan en modo register. El campo de términos y condiciones (Checkbox) también es exclusivo del registro. + +Los `socialButtons` se renderizan con un Divider "O continúa con email" cuando están presentes. Sin socialButtons el Divider no aparece. + +El campo `password` usa PasswordInput de Mantine, que incluye el toggle de visibilidad integrado. diff --git a/components/auth_form.tsx b/components/auth_form.tsx new file mode 100644 index 0000000..4d05f42 --- /dev/null +++ b/components/auth_form.tsx @@ -0,0 +1,181 @@ +import * as React from 'react' +import { + Anchor, + Button, + Checkbox, + Divider, + Group, + Paper, + PasswordInput, + Stack, + Text, + TextInput, + Title, + Container, + type PaperProps, +} from '@mantine/core' +import { useToggle, upperFirst } from '@mantine/hooks' + +interface SocialButtonConfig { + label: string + icon?: React.ReactNode + onClick?: () => void +} + +interface ExtraFieldConfig { + name: string + label: string + placeholder?: string + required?: boolean +} + +interface AuthFormSubmitValues { + type: 'login' | 'register' + email: string + password: string + [key: string]: unknown +} + +interface AuthFormConfig { + /** Título principal de la página */ + title?: string + /** Botones de autenticación social opcionales */ + socialButtons?: SocialButtonConfig[] + /** Campos adicionales que se muestran solo en el modo registro */ + extraFields?: ExtraFieldConfig[] + /** Callback invocado al enviar el formulario */ + onSubmit?: (values: AuthFormSubmitValues) => void + /** Modo inicial: 'login' (default) o 'register' */ + defaultType?: 'login' | 'register' + /** Props adicionales para el Paper contenedor */ + paperProps?: PaperProps +} + +function AuthForm({ + title = 'Welcome', + socialButtons = [], + extraFields = [], + onSubmit, + defaultType = 'login', + paperProps, +}: AuthFormConfig): React.ReactElement { + const [type, toggle] = useToggle<'login' | 'register'>([ + defaultType, + defaultType === 'login' ? 'register' : 'login', + ]) + const [email, setEmail] = React.useState('') + const [password, setPassword] = React.useState('') + const [terms, setTerms] = React.useState(true) + const [extraValues, setExtraValues] = React.useState>({}) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + onSubmit?.({ type, email, password, ...extraValues }) + } + + const handleExtraChange = (name: string, value: string) => { + setExtraValues((prev) => ({ ...prev, [name]: value })) + } + + return ( + + + + {title} + + + {socialButtons.length > 0 && ( + <> + + {socialButtons.map((btn) => ( + + ))} + + + + )} + +
+ + {type === 'register' && + extraFields.map((field) => ( + handleExtraChange(field.name, e.currentTarget.value)} + radius="md" + /> + ))} + + setEmail(e.currentTarget.value)} + radius="md" + /> + + setPassword(e.currentTarget.value)} + radius="md" + /> + + {type === 'register' && ( + setTerms(e.currentTarget.checked)} + /> + )} + + + + toggle()} + > + {type === 'register' + ? '¿Ya tienes cuenta? Inicia sesión' + : '¿No tienes cuenta? Regístrate'} + + + +
+ + {type === 'register' && ( + + Al registrarte aceptas nuestra{' '} + + política de privacidad + + . + + )} +
+
+ ) +} + +export { AuthForm } +export type { AuthFormConfig, AuthFormSubmitValues, SocialButtonConfig, ExtraFieldConfig } diff --git a/components/autocomplete.md b/components/autocomplete.md new file mode 100644 index 0000000..dc4f301 --- /dev/null +++ b/components/autocomplete.md @@ -0,0 +1,136 @@ +--- +name: autocomplete +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Autocomplete(props: AutocompleteProps): JSX.Element" +description: "Input con sugerencias de autocompletado. Permite valores libres a diferencia de Select. Wrapper sobre Mantine Autocomplete." +tags: [autocomplete, input, form, suggestions, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["@mantine/core"] +output: "Componente Autocomplete que renderiza input con dropdown de sugerencias filtradas" +props: + - name: data + type: "string[] | { value: string; label?: string; group?: string }[]" + required: true + description: "Lista de opciones a mostrar en el dropdown" + - name: value + type: "string" + required: false + description: "Valor controlado del input" + - name: onChange + type: "(value: string) => void" + required: false + description: "Callback al cambiar el valor del input" + - name: label + type: "string" + required: false + description: "Etiqueta visible encima del input" + - name: placeholder + type: "string" + required: false + description: "Texto placeholder cuando el input está vacío" + - name: clearable + type: "boolean" + required: false + description: "Muestra botón para limpiar el valor" + - name: loading + type: "boolean" + required: false + description: "Muestra spinner de carga en el input" + - name: disabled + type: "boolean" + required: false + description: "Deshabilita el input" + - name: limit + type: "number" + required: false + description: "Número máximo de sugerencias a mostrar en el dropdown" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño visual del input" +emits: [onChange] +has_state: true +framework: react +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/autocomplete.tsx" +--- + +## Ejemplo + +```tsx +import { Autocomplete } from '@fn_library/autocomplete' + +// Básico — lista de strings +function BasicAutocomplete() { + return ( + + ) +} + +// Con grupos +function GroupedAutocomplete() { + return ( + + ) +} + +// Con loading y clearable (búsqueda asíncrona) +function AsyncAutocomplete() { + const [value, setValue] = useState('') + const [data, setData] = useState([]) + const [loading, setLoading] = useState(false) + + const handleChange = async (val: string) => { + setValue(val) + if (val.length < 2) return + setLoading(true) + const results = await fetchSuggestions(val) + setData(results) + setLoading(false) + } + + return ( + + ) +} +``` + +## Notas + +A diferencia de `Select`, `Autocomplete` permite que el usuario ingrese cualquier valor libre, no solo los de la lista. Ideal para búsquedas con sugerencias donde el valor final puede no estar en el dataset. + +Cuando `data` contiene objetos con `group`, el dropdown agrupa visualmente las opciones bajo el encabezado del grupo. + +El prop `limit` controla cuántas sugerencias se muestran simultáneamente (por defecto Mantine muestra todas). Útil para datasets grandes o búsquedas asíncronas donde se quiere limitar el ruido visual. diff --git a/components/autocomplete.tsx b/components/autocomplete.tsx new file mode 100644 index 0000000..d60099a --- /dev/null +++ b/components/autocomplete.tsx @@ -0,0 +1,10 @@ +import { Autocomplete as MantineAutocomplete, type AutocompleteProps as MantineAutcompleteProps } from '@mantine/core' + +interface AutocompleteProps extends MantineAutcompleteProps {} + +function Autocomplete(props: AutocompleteProps) { + return +} + +export { Autocomplete } +export type { AutocompleteProps } diff --git a/components/avatar.md b/components/avatar.md new file mode 100644 index 0000000..50c698d --- /dev/null +++ b/components/avatar.md @@ -0,0 +1,71 @@ +--- +name: avatar +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Avatar(props: AvatarProps): JSX.Element" +description: "Imagen de usuario circular con fallback a iniciales generadas automaticamente. 5 tamaños via Mantine Avatar." +tags: [avatar, user, image, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Avatar que renderiza imagen de usuario circular con fallback a iniciales generadas" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/avatar.tsx" +props: + - name: src + type: "string" + required: false + description: "URL de la imagen" + - name: alt + type: "string" + required: false + description: "Texto alternativo de la imagen" + - name: fallback + type: "string" + required: false + description: "Nombre completo del que extraer iniciales (ej: 'Juan Perez' -> 'JP')" + - name: initials + type: "string" + required: false + description: "Iniciales explicitas para el fallback (sobrescribe fallback)" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamanio del avatar (default: md)" + - name: className + type: "string" + required: false + description: "Clases CSS adicionales" +emits: [] +has_state: false +framework: react +variant: [xs, sm, md, lg, xl] +--- + +## Ejemplo + +```tsx +// Con imagen + + +// Con fallback a iniciales + + +// Iniciales explicitas + + +// Maneja error de imagen automaticamente + +``` + +## Notas + +Usa Mantine Avatar que maneja errores de carga de imagen nativamente. La funcion getInitials extrae 2 iniciales del nombre completo (primera y ultima palabra). Si solo hay una palabra, toma los 2 primeros caracteres. Usa forwardRef para compatibilidad con wrappers. diff --git a/components/avatar.tsx b/components/avatar.tsx new file mode 100644 index 0000000..6e43ec8 --- /dev/null +++ b/components/avatar.tsx @@ -0,0 +1,57 @@ +import * as React from 'react' +import { Avatar as MantineAvatar } from '@mantine/core' + +type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +const sizeMap: Record = { + xs: 'sm', + sm: 'sm', + md: 'md', + lg: 'lg', + xl: 'xl', +} + +interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> { + src?: string + alt?: string + fallback?: string + initials?: string + size?: AvatarSize +} + +function getInitials(name?: string): string { + if (!name) return '?' + const parts = name.trim().split(/\s+/) + const first = parts[0] ?? '' + const last = parts[parts.length - 1] ?? '' + if (parts.length === 1) return first.slice(0, 2).toUpperCase() + return ((first[0] ?? '') + (last[0] ?? '')).toUpperCase() +} + +/** Kept for backwards compatibility */ +const avatarVariants = sizeMap + +const Avatar = React.forwardRef( + ({ className, size = 'md', src, alt, fallback, initials, ...props }, ref) => { + const displayInitials = initials ?? getInitials(fallback ?? alt) + + return ( + + {displayInitials} + + ) + } +) +Avatar.displayName = 'Avatar' + +export { Avatar, avatarVariants } +export type { AvatarProps, AvatarSize } diff --git a/components/badge.md b/components/badge.md new file mode 100644 index 0000000..2944e7e --- /dev/null +++ b/components/badge.md @@ -0,0 +1,49 @@ +--- +name: badge +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Badge(props: BadgeProps & VariantProps): JSX.Element" +description: "Badge con 10 variantes semánticas (default, secondary, destructive, outline, ghost, link, success, warning, error, info) y 2 tamaños. Mantine Badge." +tags: [badge, status, component, ui, indicator, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Badge que renderiza un indicador visual con 10 variantes semánticas de estado" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/badge.tsx" +props: + - name: variant + type: "'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'success' | 'warning' | 'error' | 'info'" + required: false + description: "Variante visual" + - name: size + type: "'default' | 'sm'" + required: false + description: "Tamaño" +emits: [] +has_state: false +framework: react +variant: [default, secondary, destructive, outline, ghost, link, success, warning, error, info] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/badge.tsx" +--- + +## Ejemplo + +```tsx +Active +Error +``` + +## Notas + +Usa Mantine Badge internamente. Las 10 variantes se mapean a combinaciones de variant+color de Mantine (filled, light, outline, subtle, transparent). diff --git a/components/badge.tsx b/components/badge.tsx new file mode 100644 index 0000000..5689a09 --- /dev/null +++ b/components/badge.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { Badge as MantineBadge } from '@mantine/core' + +type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'success' | 'warning' | 'error' | 'info' +type BadgeSize = 'default' | 'sm' + +const variantMap: Record = { + default: { variant: 'filled' }, + secondary: { variant: 'light' }, + destructive: { variant: 'light', color: 'red' }, + outline: { variant: 'outline' }, + ghost: { variant: 'subtle' }, + link: { variant: 'transparent' }, + success: { variant: 'light', color: 'green' }, + warning: { variant: 'light', color: 'yellow' }, + error: { variant: 'light', color: 'red' }, + info: { variant: 'light', color: 'blue' }, +} + +/** Kept for backwards compatibility */ +const badgeVariants = variantMap + +interface BadgeProps extends React.HTMLAttributes { + variant?: BadgeVariant + size?: BadgeSize +} + +function Badge({ className, variant = 'default', size = 'default', children, ...props }: BadgeProps) { + const mv = variantMap[variant] + + return ( + + {children} + + ) +} + +export { Badge, badgeVariants } +export type { BadgeProps, BadgeVariant, BadgeSize } diff --git a/components/bar_chart.md b/components/bar_chart.md new file mode 100644 index 0000000..e9e7bd7 --- /dev/null +++ b/components/bar_chart.md @@ -0,0 +1,57 @@ +--- +name: bar_chart +kind: component +lang: ts +domain: ui +version: "1.1.0" +purity: impure +signature: "BarChart(props: BarChartProps): JSX.Element" +description: "Gráfico de barras @mantine/charts con multi-series, orientación horizontal/vertical y tooltips." +tags: [chart, bar, visualization, mantine, component, ui] +uses_functions: [chart_container_ts_ui] +uses_types: [ChartSeries_ts_ui] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/charts", "@mantine/core"] +output: "Componente JSX que renderiza un gráfico de barras vertical u horizontal con multi-series y tooltips" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/bar_chart.tsx" +props: + - name: data + type: "Record[]" + required: true + description: "Array de datos" + - name: xKey + type: "string" + required: true + description: "Key del eje X/categoría" + - name: horizontal + type: "boolean" + required: false + description: "Orientación horizontal" + - name: series + type: "Series[]" + required: false + description: "Series de datos para multi-series" +emits: [] +has_state: false +framework: react +variant: [vertical, horizontal] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/charts/bar-chart.tsx" +--- + +## Ejemplo + +```tsx + + +``` + +## Notas + +En modo `horizontal=true` se pasa `orientation="vertical"` a Mantine BarChart, que internamente intercambia los ejes. diff --git a/components/bar_chart.tsx b/components/bar_chart.tsx new file mode 100644 index 0000000..066339e --- /dev/null +++ b/components/bar_chart.tsx @@ -0,0 +1,47 @@ +import { BarChart as MantineBarChart } from '@mantine/charts' +import { Paper } from '@mantine/core' +import { type Series, getSeriesColor } from './chart_container' + +interface BarChartProps { + data: Record[] + xKey: string + yKey?: string + series?: Series[] + horizontal?: boolean + showGrid?: boolean + showLegend?: boolean + height?: number + xAxisFormatter?: (value: unknown) => string + yAxisFormatter?: (value: unknown) => string + valueFormatter?: (value: number) => string +} + +function BarChartComponent({ + data, xKey, yKey, series, horizontal = false, showGrid = true, showLegend = false, + height = 300, xAxisFormatter, yAxisFormatter, valueFormatter = (v) => v.toLocaleString(), +}: BarChartProps) { + const chartSeries = series + ? series.map((s, i) => ({ name: s.key, label: s.name, color: getSeriesColor(i, s.color) })) + : yKey ? [{ name: yKey, label: yKey, color: getSeriesColor(0) }] : [] + + return ( + + + + ) +} + +export const BarChart = BarChartComponent +export type { BarChartProps } diff --git a/components/breadcrumb.md b/components/breadcrumb.md new file mode 100644 index 0000000..19decd7 --- /dev/null +++ b/components/breadcrumb.md @@ -0,0 +1,72 @@ +--- +name: breadcrumb +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Breadcrumb(props: BreadcrumbProps): JSX.Element" +description: "Navegacion jerarquica con separadores, elipsis para paths largos y soporte para router links via asChild. Mantine Anchor/Text." +tags: [breadcrumb, navigation, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", "@tabler/icons-react"] +output: "Componente Breadcrumb que renderiza navegación jerárquica con separadores, elipsis y soporte para router links" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/breadcrumb.tsx" +props: + - name: className + type: "string" + required: false + description: "Clases CSS adicionales" +emits: [] +has_state: false +framework: react +variant: [] +--- + +## Ejemplo + +```tsx + + + + Inicio + + + + Documentacion + + + + Componentes + + + + +// Con elipsis para paths largos + + + + Inicio + + + + + + + + Pagina actual + + + +``` + +## Notas + +Exports: Breadcrumb (nav), BreadcrumbList (ol via Group), BreadcrumbItem (li via Group), BreadcrumbLink (Mantine Anchor con asChild), BreadcrumbPage (Text aria-current=page), BreadcrumbSeparator (IconChevronRight por defecto, customizable), BreadcrumbEllipsis (IconDots). BreadcrumbLink acepta asChild para usar con Link de React Router o Next.js. Usa Tabler icons en vez de lucide-react. diff --git a/components/breadcrumb.tsx b/components/breadcrumb.tsx new file mode 100644 index 0000000..f40a765 --- /dev/null +++ b/components/breadcrumb.tsx @@ -0,0 +1,95 @@ +import * as React from "react" +import { Anchor, Text, Box } from "@mantine/core" +import { IconChevronRight, IconDots } from "@tabler/icons-react" + +function Breadcrumb({ children, ...props }: React.ComponentPropsWithoutRef<"nav">) { + return +} + +function BreadcrumbList({ className, children, ...props }: React.ComponentPropsWithoutRef<"ol">) { + return ( +
    + {children} +
+ ) +} + +function BreadcrumbItem({ className, children, ...props }: React.ComponentPropsWithoutRef<"li">) { + return ( +
  • + {children} +
  • + ) +} + +function BreadcrumbLink({ + className, + href, + asChild, + children, + ...props +}: React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }) { + if (asChild) { + return ( + )}> + {children} + + ) + } + return ( + + {children} + + ) +} + +function BreadcrumbPage({ className, ...props }: React.ComponentPropsWithoutRef<"span">) { + return ( + + ) +} + +function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) { + return ( + + ) +} + +function BreadcrumbEllipsis({ className, ...props }: React.ComponentPropsWithoutRef<"span">) { + return ( + + ) +} + +export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } diff --git a/components/button.md b/components/button.md new file mode 100644 index 0000000..ccbe113 --- /dev/null +++ b/components/button.md @@ -0,0 +1,54 @@ +--- +name: button +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Button(props: ButtonProps & VariantProps): JSX.Element" +description: "Botón accesible con 6 variantes (default, outline, secondary, ghost, destructive, link) y 8 tamaños. Mantine Button." +tags: [button, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "JSX.Element: botón renderizado con los estilos y comportamientos configurados" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/button.tsx" +props: + - name: variant + type: "'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link'" + required: false + description: "Estilo visual del botón" + - name: size + type: "'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'" + required: false + description: "Tamaño del botón" + - name: className + type: "string" + required: false + description: "Clases CSS adicionales" +emits: [onClick] +has_state: false +framework: react +variant: [default, outline, secondary, ghost, destructive, link] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/button.tsx" +--- + +## Ejemplo + +```tsx + + + +``` + +## Notas + +Componente base del sistema. Usa Mantine Button para accesibilidad completa (keyboard, ARIA). Las variantes se mapean a Mantine: default->filled, outline->outline, secondary->light, ghost->subtle, destructive->filled(red), link->transparent. diff --git a/components/button.tsx b/components/button.tsx new file mode 100644 index 0000000..e8dbdf3 --- /dev/null +++ b/components/button.tsx @@ -0,0 +1,64 @@ +import * as React from 'react' +import { Button as MantineButton } from '@mantine/core' + +type ButtonVariant = 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link' +type ButtonSize = 'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg' + +const variantMap: Record = { + default: { variant: 'filled' }, + outline: { variant: 'outline' }, + secondary: { variant: 'light' }, + ghost: { variant: 'subtle' }, + destructive: { variant: 'filled', color: 'red' }, + link: { variant: 'transparent' }, +} + +const sizeMap: Record = { + default: { size: 'sm' }, + xs: { size: 'xs' }, + sm: { size: 'xs' }, + lg: { size: 'md' }, + icon: { size: 'sm', style: { width: 32, height: 32, padding: 0 } }, + 'icon-xs': { size: 'xs', style: { width: 24, height: 24, padding: 0 } }, + 'icon-sm': { size: 'xs', style: { width: 28, height: 28, padding: 0 } }, + 'icon-lg': { size: 'md', style: { width: 36, height: 36, padding: 0 } }, +} + +/** Kept for backwards compatibility — maps variant names to Mantine equivalents */ +const buttonVariants = variantMap + +interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { + variant?: ButtonVariant + size?: ButtonSize + children?: React.ReactNode +} + +function Button({ + className, + variant = 'default', + size = 'default', + style, + children, + ...props +}: ButtonProps) { + const mv = variantMap[variant] + const ms = sizeMap[size] + + return ( + + {children} + + ) +} + +export { Button, buttonVariants } +export type { ButtonProps, ButtonVariant, ButtonSize } diff --git a/components/card.md b/components/card.md new file mode 100644 index 0000000..5fec2f8 --- /dev/null +++ b/components/card.md @@ -0,0 +1,71 @@ +--- +name: card +kind: component +lang: ts +domain: ui +version: "1.1.0" +purity: impure +signature: "Card(props: { size?: 'default' | 'sm'; variant?: 'default' | 'borderless' | 'ghost'; className?: string; children: ReactNode }): JSX.Element" +description: "Contenedor card con header, title, description, action, content y footer. Sistema de slots composable. Variantes default, borderless y ghost para dashboards dark." +tags: [card, container, layout, component, ui, dashboard, dark] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["react"] +output: "Componente Card que renderiza un contenedor con slots composables (header, content, footer) y 3 variantes visuales" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/card.tsx" +props: + - name: size + type: "'default' | 'sm'" + required: false + description: "Tamaño del card" + - name: variant + type: "'default' | 'borderless' | 'ghost'" + required: false + description: "Variante visual. borderless quita borde/shadow, ghost además hace bg transparente" + - name: className + type: "string" + required: false + description: "Clases CSS adicionales" +emits: [] +has_state: false +framework: react +variant: [default, sm, borderless, ghost] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/card.tsx" +--- + +## Ejemplo + +```tsx + + + Título + Descripción + + Contenido + Footer + + +{/* Dashboard dark — sin bordes */} + + Widget sin marco + + +{/* Completamente transparente */} + + Sin fondo ni borde + +``` + +## Notas + +Sistema de slots via data-slot attributes. Card detecta automáticamente la presencia de CardFooter y ajusta el padding. Exporta 7 subcomponentes composables. + +Las variantes `borderless` y `ghost` eliminan el `ring-1` del borde por defecto. `ghost` además hace el fondo transparente. Alternativa con CSS global: `[data-slot="card"] { --tw-ring-opacity: 0; }` o `[data-variant="borderless"] { ring: 0 }` via `data-variant` attribute expuesto. diff --git a/components/card.tsx b/components/card.tsx new file mode 100644 index 0000000..13e4463 --- /dev/null +++ b/components/card.tsx @@ -0,0 +1,102 @@ +import * as React from 'react' +import { Paper, Box, Text } from '@mantine/core' + +type CardVariant = 'default' | 'borderless' | 'ghost' + +function Card({ + className, + size = 'default', + variant = 'default', + children, + ...props +}: React.ComponentProps<'div'> & { size?: 'default' | 'sm'; variant?: CardVariant }) { + return ( + + {children} + + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +function CardAction({ className, style, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( + + ) +} + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent } diff --git a/components/chart_container.md b/components/chart_container.md new file mode 100644 index 0000000..42ba482 --- /dev/null +++ b/components/chart_container.md @@ -0,0 +1,52 @@ +--- +name: chart_container +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "ChartContainer(props: { children: ReactNode; height?: number | string }): JSX.Element" +description: "Thin wrapper Paper y utilidades de colores/series para los charts @mantine/charts." +tags: [chart, container, mantine, base, visualization, component, ui] +uses_functions: [] +uses_types: [ChartSeries_ts_ui] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente ChartContainer Paper wrapper y utilidades getSeriesColor/Series para charts Mantine" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/chart_container.tsx" +props: + - name: height + type: "number | string" + required: false + description: "Altura del chart (default 300)" + - name: className + type: "string" + required: false + description: "Clases CSS adicionales" +emits: [] +has_state: false +framework: react +variant: [default] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/charts/chart-base.tsx" +--- + +## Ejemplo + +```tsx +import { ChartContainer, getSeriesColor, type Series } from './chart_container' + + + + +``` + +## Notas + +Exporta: ChartContainer, defaultColors, getSeriesColor, Series. Wrapper fino sobre Mantine Paper para layout uniforme de charts. diff --git a/components/chart_container.tsx b/components/chart_container.tsx new file mode 100644 index 0000000..1c1a352 --- /dev/null +++ b/components/chart_container.tsx @@ -0,0 +1,33 @@ +import { Paper } from '@mantine/core' + +export const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899'] + +export interface Series { + key: string + name: string + color?: string +} + +export function getSeriesColor(index: number, color?: string): string { + return color || defaultColors[index % defaultColors.length]! +} + +interface ChartContainerProps { + children: React.ReactNode + height?: number | string +} + +export function ChartContainer({ children, height = 300 }: ChartContainerProps) { + return ( + + {children} + + ) +} + +/** @deprecated Mantine charts handle tooltips internally. Kept for index.ts compat. */ +export function ChartTooltipContent() { return null } +/** @deprecated Mantine charts handle tooltips internally. Kept for index.ts compat. */ +export function ChartTooltip() { return null } +/** @deprecated Mantine charts handle legends internally. Kept for index.ts compat. */ +export function ChartLegend() { return null } diff --git a/components/checkbox.md b/components/checkbox.md new file mode 100644 index 0000000..bb425ae --- /dev/null +++ b/components/checkbox.md @@ -0,0 +1,73 @@ +--- +name: checkbox +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Checkbox(props: CheckboxProps): JSX.Element" +description: "Input booleano accesible con label opcional y variante indeterminate. Mantine Checkbox." +tags: [checkbox, component, ui, interactive, form, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Checkbox que renderiza input booleano accesible con label opcional y estado indeterminate" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/checkbox.tsx" +props: + - name: label + type: "string" + required: false + description: "Texto de etiqueta visible junto al checkbox" + - name: indeterminate + type: "boolean" + required: false + description: "Estado indeterminate (guion) en vez de checked/unchecked" + - name: checked + type: "boolean" + required: false + description: "Estado controlado del checkbox" + - name: defaultChecked + type: "boolean" + required: false + description: "Estado inicial no controlado" + - name: disabled + type: "boolean" + required: false + description: "Deshabilita el checkbox" + - name: onCheckedChange + type: "(checked: boolean) => void" + required: false + description: "Callback cuando cambia el estado" +emits: [onCheckedChange] +has_state: false +framework: react +variant: [] +--- + +## Ejemplo + +```tsx +// Basico + + +// Controlado + + +// Sin label + +``` + +## Notas + +Usa Mantine Checkbox para accesibilidad completa (keyboard, ARIA). El estado indeterminate se muestra con un guion horizontal. El callback onCheckedChange se adapta desde el onChange nativo de Mantine. diff --git a/components/checkbox.tsx b/components/checkbox.tsx new file mode 100644 index 0000000..a920502 --- /dev/null +++ b/components/checkbox.tsx @@ -0,0 +1,34 @@ +import { Checkbox as MantineCheckbox } from "@mantine/core" + +interface CheckboxProps { + label?: string + indeterminate?: boolean + className?: string + labelClassName?: string + checked?: boolean + defaultChecked?: boolean + disabled?: boolean + onCheckedChange?: (checked: boolean) => void + id?: string +} + +function Checkbox({ className, label, id, indeterminate, checked, defaultChecked, disabled, onCheckedChange, ...props }: CheckboxProps) { + return ( + onCheckedChange?.(event.currentTarget.checked)} + className={className} + size="sm" + {...props} + /> + ) +} + +export { Checkbox } +export type { CheckboxProps } diff --git a/components/chip.md b/components/chip.md new file mode 100644 index 0000000..6d21169 --- /dev/null +++ b/components/chip.md @@ -0,0 +1,88 @@ +--- +name: chip +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Chip(props: ChipProps): JSX.Element" +description: "Chip seleccionable con variantes filled/outline/light. ChipGroup para selección simple o múltiple. Wrapper sobre Mantine Chip." +tags: [chip, toggle, selection, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Chip que renderiza un elemento seleccionable tipo badge, con ChipGroup para gestionar selección simple o múltiple entre varios chips" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/chip.tsx" +props: + - name: checked + type: "boolean" + required: false + description: "Estado seleccionado del chip (controlled)" + - name: onChange + type: "(checked: boolean) => void" + required: false + description: "Callback al cambiar el estado del chip" + - name: variant + type: "'filled' | 'outline' | 'light'" + required: false + description: "Estilo visual del chip" + - name: color + type: "string" + required: false + description: "Color del chip cuando está seleccionado" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño del chip" + - name: children + type: "React.ReactNode" + required: true + description: "Contenido del chip — texto o elemento" +emits: [onChange] +has_state: true +framework: react +variant: [filled, outline, light] +--- + +## Ejemplo + +```tsx +import { Chip, ChipGroup } from '@fn_library' + +// Chip individual controlado + + Activo + + +// ChipGroup selección simple (una sola opción) + + React + Vue + Svelte + + +// ChipGroup selección múltiple + + Frontend + Backend + DevOps + + +// Con variante y color custom + + Destacado + +``` + +## Notas + +- Wrapper directo sobre `Chip` de `@mantine/core` v9. Todas las props de Mantine Chip son válidas. +- `ChipGroup` es un alias de `MantineChip.Group` — gestiona el estado de selección entre chips hijos. +- En `ChipGroup` sin `multiple`, `value` es `string`. Con `multiple`, `value` es `string[]`. +- Internamente cada `Chip` renderiza un checkbox/radio accesible oculto con label visual. diff --git a/components/chip.tsx b/components/chip.tsx new file mode 100644 index 0000000..adcc49e --- /dev/null +++ b/components/chip.tsx @@ -0,0 +1,12 @@ +import { Chip as MantineChip, type ChipProps as MantineChipProps } from '@mantine/core' + +interface ChipProps extends MantineChipProps {} + +function Chip(props: ChipProps) { + return +} + +const ChipGroup = MantineChip.Group + +export { Chip, ChipGroup } +export type { ChipProps } diff --git a/components/color_input.md b/components/color_input.md new file mode 100644 index 0000000..901270b --- /dev/null +++ b/components/color_input.md @@ -0,0 +1,104 @@ +--- +name: color_input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "ColorInput(props: ColorInputProps): JSX.Element" +description: "Selector de color con picker, swatches predefinidos y eye dropper. Soporta hex, rgb, hsl con alpha. Wrapper sobre Mantine ColorInput." +tags: [color, picker, input, form, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/color_input.tsx" +framework: react +has_state: true +props: + - name: format + type: "\"hex\" | \"hexa\" | \"rgb\" | \"rgba\" | \"hsl\" | \"hsla\"" + required: false + description: "Formato de color a usar en el valor. Por defecto hex." + - name: swatches + type: "string[]" + required: false + description: "Lista de colores predefinidos mostrados como swatches en el picker." + - name: withEyeDropper + type: "boolean" + required: false + description: "Muestra el boton eye dropper para seleccionar color de la pantalla." + - name: withPicker + type: "boolean" + required: false + description: "Muestra el color picker interactivo. Por defecto true." + - name: value + type: "string" + required: false + description: "Valor controlado del color en el formato especificado." + - name: onChange + type: "(value: string) => void" + required: false + description: "Callback invocado cuando el color cambia." + - name: label + type: "React.ReactNode" + required: false + description: "Etiqueta del campo." + - name: placeholder + type: "string" + required: false + description: "Placeholder del input de texto." + - name: disabled + type: "boolean" + required: false + description: "Deshabilita el input." + - name: size + type: "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"" + required: false + description: "Tamanio del componente." +emits: [onChange, onChangeEnd] +output: "Componente ColorInput que renderiza input de color con picker desplegable y swatches" +params: [] +--- + +## Ejemplo + +```tsx +import { ColorInput } from '@fn_library/color_input' + +// Basico con hex (default) + + +// Con swatches predefinidos + + +// Con rgba y eye dropper + console.log(value)} +/> +``` + +## Notas + +Wrapper directo sobre `ColorInput` de Mantine v9. Hereda todas las props de Mantine sin restricciones. + +El eye dropper (`withEyeDropper`) solo funciona en browsers que soporten la EyeDropper API (Chrome/Edge). En Firefox no aparece el boton automaticamente. + +Cuando `format` es `hex` o `hsl`, el valor no incluye canal alpha. Para transparencia usar `hexa`, `rgba` o `hsla`. diff --git a/components/color_input.tsx b/components/color_input.tsx new file mode 100644 index 0000000..3d34d95 --- /dev/null +++ b/components/color_input.tsx @@ -0,0 +1,10 @@ +import { ColorInput as MantineColorInput, type ColorInputProps as MantineColorInputProps } from '@mantine/core' + +interface ColorInputProps extends MantineColorInputProps {} + +function ColorInput(props: ColorInputProps) { + return +} + +export { ColorInput } +export type { ColorInputProps } diff --git a/components/command.md b/components/command.md new file mode 100644 index 0000000..0c01041 --- /dev/null +++ b/components/command.md @@ -0,0 +1,82 @@ +--- +name: command +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Command(props: CommandProps): JSX.Element" +description: "Combobox de busqueda y seleccion estilo cmdk. Filtra items por query, soporta grupos, iconos y shortcuts. Incluye CommandSearch para uso de una linea." +tags: [command, search, combobox, component, ui, interactive] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", "@tabler/icons-react"] +output: "Componente Command que renderiza combobox de búsqueda y selección con filtrado reactivo, grupos e iconos" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/command.tsx" +props: + - name: items + type: "CommandItem[]" + required: true + description: "Array de items con value, label, description, icon, disabled, group" + - name: value + type: "string" + required: false + description: "Valor seleccionado (controlado)" + - name: onValueChange + type: "(value: string) => void" + required: false + description: "Callback al seleccionar un item" + - name: placeholder + type: "string" + required: false + description: "Placeholder del input de busqueda (default: Search...)" + - name: emptyMessage + type: "string" + required: false + description: "Mensaje cuando no hay resultados (default: No results found.)" +emits: [onValueChange] +has_state: true +framework: react +variant: [] +--- + +## Ejemplo + +```tsx +// Uso simple con CommandSearch +const items = [ + { value: "react", label: "React", group: "Frameworks" }, + { value: "vue", label: "Vue", group: "Frameworks" }, + { value: "typescript", label: "TypeScript", group: "Lenguajes" }, +] + + console.log(val)} +/> + +// Composable para mayor control + + setQuery(e.target.value)} /> + + Sin resultados. + + setSelected("1")}> + Opcion 1 + ⌘K + + + + +``` + +## Notas + +Implementacion propia (sin dependencia de cmdk) usando primitivos HTML nativos. CommandSearch es el wrapper de alto nivel con filtrado reactivo integrado. El filtrado es case-insensitive sobre label, description y value. Los grupos se renderizan en orden de aparicion en items. diff --git a/components/command.tsx b/components/command.tsx new file mode 100644 index 0000000..7605394 --- /dev/null +++ b/components/command.tsx @@ -0,0 +1,189 @@ +import * as React from 'react' +import { TextInput, Text, Box, ScrollArea } from '@mantine/core' +import { IconSearch } from '@tabler/icons-react' + +interface CommandItemData { + value: string + label: string + description?: string + icon?: React.ReactNode + disabled?: boolean + group?: string +} + +interface CommandProps { + items: CommandItemData[] + value?: string + onValueChange?: (value: string) => void + placeholder?: string + emptyMessage?: string + className?: string + inputClassName?: string + listClassName?: string +} + +function Command({ className, children, ...props }: React.ComponentPropsWithoutRef<'div'>) { + return {children} +} + +function CommandInput({ className, value, onChange, placeholder, ...props }: { + className?: string + value?: string + onChange?: (e: React.ChangeEvent) => void + placeholder?: string +}) { + return ( + } + className={className} + value={value} + onChange={onChange} + placeholder={placeholder} + styles={{ input: { border: 'none', borderBottom: '1px solid var(--mantine-color-default-border)' } }} + {...props} + /> + ) +} + +function CommandList({ className, children }: { className?: string; children?: React.ReactNode }) { + return ( + + {children} + + ) +} + +function CommandEmpty({ className, children }: { className?: string; children?: React.ReactNode }) { + return ( + + {children} + + ) +} + +function CommandGroup({ className, heading, children }: { className?: string; heading?: string; children?: React.ReactNode }) { + return ( + + {heading && {heading}} +
    {children}
    +
    + ) +} + +function CommandSeparator({ className }: { className?: string }) { + return +} + +function CommandItem({ className, selected, disabled, onSelect, children }: { + className?: string + selected?: boolean + disabled?: boolean + onSelect?: () => void + children?: React.ReactNode +}) { + return ( + + {children} + + ) +} + +function CommandShortcut({ className, children }: { className?: string; children?: React.ReactNode }) { + return {children} +} + +function CommandSearch({ + items, + value, + onValueChange, + placeholder = 'Search...', + emptyMessage = 'No results found.', + className, +}: CommandProps) { + const [query, setQuery] = React.useState('') + const [selectedValue, setSelectedValue] = React.useState(value ?? '') + + const filtered = React.useMemo(() => { + if (!query) return items + const q = query.toLowerCase() + return items.filter( + (item) => + item.label.toLowerCase().includes(q) || + item.description?.toLowerCase().includes(q) || + item.value.toLowerCase().includes(q) + ) + }, [items, query]) + + const groups = React.useMemo(() => { + const map = new Map() + for (const item of filtered) { + const key = item.group ?? '' + if (!map.has(key)) map.set(key, []) + map.get(key)!.push(item) + } + return map + }, [filtered]) + + const handleSelect = (val: string) => { + setSelectedValue(val) + onValueChange?.(val) + } + + return ( + + setQuery(e.target.value)} + placeholder={placeholder} + /> + + {filtered.length === 0 ? ( + {emptyMessage} + ) : ( + Array.from(groups.entries()).map(([group, groupItems]) => ( + + {groupItems.map((item) => ( + handleSelect(item.value)} + > + {item.icon && {item.icon}} + {item.label} + {item.description && ( + {item.description} + )} + + ))} + + )) + )} + + + ) +} + +export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut } +export type { CommandItemData, CommandProps } diff --git a/components/crud_page.md b/components/crud_page.md new file mode 100644 index 0000000..f1da5d7 --- /dev/null +++ b/components/crud_page.md @@ -0,0 +1,55 @@ +--- +name: crud_page +kind: function +lang: ts +domain: ui +version: "1.0.0" +purity: pure +signature: "crudPage(props: CrudPageProps): ReactElement" +description: "Genera una página CRUD completa con header, tabla con columnas configurables, botones de acción (add/edit/delete) y schema de formulario." +tags: [crud, page, table, form, factory, composition, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react, "@mantine/core", "@tabler/icons-react"] +params: + - name: props + desc: "Configuración CRUD: título, datos, columnas de tabla, campos de formulario y callbacks para add/edit/delete" +output: "Componente ReactElement que renderiza página CRUD completa con tabla y botones de acción" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/crud_page.tsx" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "" +--- + +## Ejemplo + +```tsx +crudPage({ + title: 'Users', + subtitle: 'Manage system users', + data: users, + fields: [ + { key: 'name', label: 'Name', type: 'text', required: true }, + { key: 'email', label: 'Email', type: 'email', required: true }, + { key: 'role', label: 'Role', type: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }] }, + ], + columns: [ + { key: 'name', label: 'Name' }, + { key: 'email', label: 'Email' }, + { key: 'role', label: 'Role', render: (v) => {v} }, + ], + onAdd: handleAdd, + onEdit: handleEdit, + onDelete: handleDelete, +}) +``` + +## Notas + +El schema de campos se almacena como data attribute para que un agente pueda leerlo y generar el formulario de diálogo correspondiente. La tabla incluye sorting visual implícito por columnas. diff --git a/components/crud_page.tsx b/components/crud_page.tsx new file mode 100644 index 0000000..0b97734 --- /dev/null +++ b/components/crud_page.tsx @@ -0,0 +1,121 @@ +import * as React from 'react' +import { Stack, Group, Title, Text, Paper, Table, Button, ActionIcon, Center } from '@mantine/core' +import { IconPlus, IconPencil, IconTrash } from '@tabler/icons-react' + +interface CrudField { + key: string + label: string + type: 'text' | 'number' | 'email' | 'select' | 'textarea' + required?: boolean + options?: Array<{ label: string; value: string }> + placeholder?: string +} + +interface CrudPageProps> { + title: string + subtitle?: string + data: T[] + fields: CrudField[] + columns: Array<{ + key: keyof T + label: string + render?: (value: unknown, row: T) => React.ReactNode + }> + onAdd?: (item: Partial) => void + onEdit?: (item: T) => void + onDelete?: (item: T) => void + actions?: React.ReactNode + className?: string +} + +export function crudPage>({ + title, + subtitle, + data, + fields, + columns, + onAdd, + onEdit, + onDelete, + actions, +}: CrudPageProps): React.ReactElement { + return ( + + {/* Header */} + + + {title} + {subtitle && {subtitle}} + + + {actions} + {onAdd && ( + + )} + + + + {/* Table */} + + + + + {columns.map((col) => ( + + {col.label} + + ))} + {(onEdit || onDelete) && ( + Actions + )} + + + + {data.length === 0 ? ( + + +
    + No items yet. +
    +
    +
    + ) : ( + data.map((row, i) => ( + + {columns.map((col) => ( + + {col.render ? col.render(row[col.key], row) : String(row[col.key] ?? '')} + + ))} + {(onEdit || onDelete) && ( + + + {onEdit && ( + onEdit(row)}> + + + )} + {onDelete && ( + onDelete(row)}> + + + )} + + + )} + + )) + )} +
    +
    +
    + + {/* Form fields definition (for agent use) */} +
    + + ) +} + +export type { CrudPageProps, CrudField } diff --git a/components/dashboard_layout.md b/components/dashboard_layout.md new file mode 100644 index 0000000..8a8f294 --- /dev/null +++ b/components/dashboard_layout.md @@ -0,0 +1,46 @@ +--- +name: dashboard_layout +kind: function +lang: ts +domain: ui +version: "1.0.0" +purity: pure +signature: "dashboardLayout(props: DashboardLayoutProps): ReactElement" +description: "Genera un grid responsive de dashboard a partir de un array de widgets con span configurable. 1-4 columnas con auto-responsive." +tags: [dashboard, layout, grid, factory, composition, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react, "@mantine/core"] +params: + - name: props + desc: "Configuración de layout: número de columnas y array de widgets con id, título, contenido y span" +output: "Componente ReactElement que renderiza grid responsive de dashboard con ancho adaptable por widget" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/dashboard_layout.tsx" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "" +--- + +## Ejemplo + +```tsx +dashboardLayout({ + columns: 4, + widgets: [ + { id: 'revenue', title: 'Revenue', content: }, + { id: 'users', title: 'Users', content: }, + { id: 'chart', title: 'Trends', span: 2, content: }, + { id: 'table', span: 4, content: }, + ] +}) +``` + +## Notas + +Factory pura — dado el mismo input siempre genera el mismo JSX. Un agente puede construir dashboards completos pasando widgets como configuración declarativa. diff --git a/components/dashboard_layout.tsx b/components/dashboard_layout.tsx new file mode 100644 index 0000000..d542653 --- /dev/null +++ b/components/dashboard_layout.tsx @@ -0,0 +1,53 @@ +import * as React from 'react' +import { SimpleGrid, Paper, Text } from '@mantine/core' + +interface DashboardWidget { + id: string + title?: string + span?: 1 | 2 | 3 | 4 + rowSpan?: 1 | 2 + content: React.ReactNode +} + +interface DashboardLayoutProps { + widgets: DashboardWidget[] + columns?: 1 | 2 | 3 | 4 + gap?: 'sm' | 'md' | 'lg' + className?: string +} + +const gapMap = { sm: 'xs', md: 'md', lg: 'lg' } as const + +export function dashboardLayout({ + widgets, + columns = 4, + gap = 'md', +}: DashboardLayoutProps): React.ReactElement { + return ( + + {widgets.map((widget) => ( + 1 ? `span ${widget.span}` : undefined, + gridRow: widget.rowSpan === 2 ? 'span 2' : undefined, + }} + > + {widget.title && ( + {widget.title} + )} + {widget.content} + + ))} + + ) +} + +export type { DashboardWidget, DashboardLayoutProps } diff --git a/components/data_table.md b/components/data_table.md new file mode 100644 index 0000000..c10e82f --- /dev/null +++ b/components/data_table.md @@ -0,0 +1,93 @@ +--- +name: data_table +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "DataTable(props: DataTableProps): JSX.Element" +description: "Tabla de datos con sticky header, overflow scroll, heatmap por columna, formato condicional (number/datetime/currency) y hover rows. Auto-detecta columnas desde la primera fila si no se proveen." +tags: [table, data, heatmap, dashboard, component, ui, format, visualization] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react, "@mantine/core"] +output: "Componente DataTable que renderiza tabla con sticky header, heatmap condicional y formato automático de datos" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/data_table.tsx" +props: + - name: data + type: "Record[]" + required: true + description: "Filas de datos. Cada objeto es una fila." + - name: columns + type: "ColumnDef[]" + required: false + description: "Definición de columnas con key, label, format y align. Si se omite, se auto-detectan desde la primera fila." + - name: heatmapColumns + type: "string[]" + required: false + description: "Keys de columnas numéricas que deben colorearse por intensidad (azul oscuro=bajo, azul claro=alto)." + - name: maxHeight + type: "number | string" + required: false + description: "Altura máxima antes de scroll. Default 500px." + - name: loading + type: "boolean" + required: false + description: "Estado de carga. Muestra spinner si data está vacía." + - name: error + type: "Error | null" + required: false + description: "Error a mostrar si la carga falló." +emits: [] +has_state: false +framework: react +variant: [default, heatmap] +--- + +## Ejemplo + +```tsx +// Tabla simple con auto-detección de columnas + + +// Con columnas definidas y heatmap + + +// Con formato moneda y fecha + +``` + +## Formatos soportados (campo `format` en ColumnDef) + +| format | Ejemplo input | Output | +|--------|--------------|--------| +| `','` | `1234567` | `1,234,567` | +| `',.2f'` | `1234.5` | `1,234.50` | +| `'$,.2f'` | `1234.5` | `$1,234.50` | +| `'.0f'` | `42.7` | `43` | +| `'datetime'` | `'2026-04-01T12:00:00Z'` | `4/1/2026, 12:00:00 PM` | + +## Notas + +Extraido y generalizado desde `apps/rapid_dashboards/frontend/src/components/widgets/TableWidget.tsx`. El heatmap usa `useMemo` para calcular min/max por columna solo cuando cambian `data` o `heatmapColumns`. La alineación de celdas numéricas es automática (derecha) cuando el valor es `typeof 'number'`; se puede sobreescribir con el campo `align` en ColumnDef. diff --git a/components/data_table.tsx b/components/data_table.tsx new file mode 100644 index 0000000..385708e --- /dev/null +++ b/components/data_table.tsx @@ -0,0 +1,155 @@ +import * as React from 'react' +import { Table, Text, Center, Loader } from '@mantine/core' + +interface ColumnDef { + key: string + label: string + /** Format string: ',.2f' | '$,.2f' | 'datetime' | ',' */ + format?: string + /** Alignment override. Numbers default to right, strings to left. */ + align?: 'left' | 'right' | 'center' +} + +interface DataTableProps { + data: Record[] + columns?: ColumnDef[] + /** Column keys that should be colored by value intensity (heatmap). */ + heatmapColumns?: string[] + maxHeight?: number | string + loading?: boolean + error?: Error | null +} + +function formatCell(value: unknown, format?: string): string { + if (value == null) return '—' + if (!format) return String(value) + + if (format === 'datetime' && !isNaN(Date.parse(String(value)))) { + return new Date(String(value)).toLocaleString() + } + + const num = Number(value) + if (!isNaN(num)) { + if (format.includes('f')) { + const match = format.match(/\.(\d+)f/) + const d = match ? parseInt(match[1]!) : 0 + let str = num.toFixed(d) + if (format.includes(',')) { + str = Number(str).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }) + } + if (format.startsWith('$')) str = '$' + str + return str + } + if (format === ',') return num.toLocaleString() + } + return String(value) +} + +function DataTableComponent({ + data, + columns, + heatmapColumns = [], + maxHeight = 500, + loading = false, + error = null, +}: DataTableProps) { + // Auto-detect columns from first row if not provided + const effectiveColumns: ColumnDef[] = (columns && columns.length > 0) + ? columns + : (data && data.length > 0) + ? Object.keys(data[0]!).map(k => ({ key: k, label: k })) + : [] + + // Compute heatmap ranges per column + const heatmapRanges = React.useMemo(() => { + const ranges: Record = {} + if (heatmapColumns.length > 0 && data && data.length > 0) { + for (const key of heatmapColumns) { + const values = data.map(r => Number(r[key])).filter(n => !isNaN(n)) + if (values.length > 0) { + ranges[key] = { min: Math.min(...values), max: Math.max(...values) } + } + } + } + return ranges + }, [data, heatmapColumns]) + + function heatmapStyle(key: string, value: unknown): React.CSSProperties | undefined { + const range = heatmapRanges[key] + if (!range || range.max === range.min) return undefined + const num = Number(value) + if (isNaN(num)) return undefined + const t = (num - range.min) / (range.max - range.min) + const alpha = 0.1 + t * 0.55 + return { backgroundColor: `rgba(59, 130, 246, ${alpha})` } + } + + if (loading && (!data || data.length === 0)) { + return ( +
    + +
    + ) + } + + if (error) { + return ( +
    + {error.message} +
    + ) + } + + return ( + + + + + {effectiveColumns.map(col => ( + + {col.label} + + ))} + + + + {(data ?? []).map((row, i) => ( + + {effectiveColumns.map(col => { + const align = col.align ?? (typeof row[col.key] === 'number' ? 'right' : 'left') + return ( + + {formatCell(row[col.key], col.format)} + + ) + })} + + ))} + +
    + {(!data || data.length === 0) && ( +
    + No data +
    + )} +
    + ) +} + +export const DataTable = DataTableComponent +export type { DataTableProps, ColumnDef } diff --git a/components/date_picker_input.md b/components/date_picker_input.md new file mode 100644 index 0000000..fde1c8d --- /dev/null +++ b/components/date_picker_input.md @@ -0,0 +1,145 @@ +--- +name: date_picker_input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "DatePickerInput(props: DatePickerInputProps): JSX.Element" +description: "Selector de fecha con input y calendario desplegable. Soporta fecha simple, múltiple y rango. Wrapper sobre Mantine DatePickerInput." +tags: [date, picker, calendar, form, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/dates"] +output: "Componente DatePickerInput que renderiza input con calendario para selección de fechas" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/date_picker_input.tsx" +props: + - name: type + type: "'default' | 'multiple' | 'range'" + required: false + description: "Modo de selección — fecha simple, múltiples fechas, o rango de fechas" + - name: value + type: "DateValue | DateValue[] | [DateValue, DateValue] | null" + required: false + description: "Fecha o fechas seleccionadas (controlled)" + - name: onChange + type: "(value: DateValue | DateValue[] | [DateValue, DateValue] | null) => void" + required: false + description: "Callback al cambiar la selección de fecha" + - name: valueFormat + type: "string" + required: false + description: "Formato de la fecha mostrada en el input (ej: 'DD/MM/YYYY')" + - name: clearable + type: "boolean" + required: false + description: "Permite limpiar la selección de fecha" + - name: label + type: "string" + required: false + description: "Label del campo" + - name: placeholder + type: "string" + required: false + description: "Texto cuando no hay fecha seleccionada" + - name: minDate + type: "Date" + required: false + description: "Fecha mínima seleccionable" + - name: maxDate + type: "Date" + required: false + description: "Fecha máxima seleccionable" + - name: disabled + type: "boolean" + required: false + description: "Deshabilitar el selector" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño del componente" +emits: [onChange] +has_state: true +framework: react +variant: [default] +params: + - name: props + desc: "Props del componente DatePickerInput — incluye type (modo de selección), value, onChange, valueFormat, clearable, label, placeholder, minDate, maxDate, disabled y size" +--- + +## Ejemplo + +```tsx +import { DatePickerInput, DatePicker } from '@fn_library' +import { useState } from 'react' + +// Fecha simple +function SingleDateExample() { + const [value, setValue] = useState(null) + return ( + + ) +} + +// Rango de fechas +function RangeDateExample() { + const [range, setRange] = useState<[Date | null, Date | null]>([null, null]) + return ( + + ) +} + +// Múltiples fechas +function MultipleDateExample() { + const [dates, setDates] = useState([]) + return ( + + ) +} + +// DatePicker inline (sin input) +function InlineDateExample() { + const [value, setValue] = useState(null) + return ( + + ) +} +``` + +## Notas + +- Wrapper directo sobre `DatePickerInput` y `DatePicker` de `@mantine/dates` v9. Todas las props de Mantine son válidas. +- Requiere importar `@mantine/dates/styles.css` — este wrapper ya lo incluye. +- El prop `type` controla el modo: `'default'` (fecha simple), `'multiple'` (varias fechas), `'range'` (rango con inicio y fin). +- `DatePicker` es el calendario inline sin input — útil para formularios donde el calendario debe estar siempre visible. +- `valueFormat` acepta tokens de dayjs (ej: `'DD/MM/YYYY'`, `'MMMM D, YYYY'`). +- Re-exporta también `DatePicker` de `@mantine/dates` con el mismo patrón de wrapper. diff --git a/components/date_picker_input.tsx b/components/date_picker_input.tsx new file mode 100644 index 0000000..f69bf0c --- /dev/null +++ b/components/date_picker_input.tsx @@ -0,0 +1,17 @@ +import { DatePickerInput as MantineDatePickerInput, DatePicker as MantineDatePicker } from '@mantine/dates' +import type { DatePickerInputProps as MantineDatePickerInputProps, DatePickerProps as MantineDatePickerProps } from '@mantine/dates' +import '@mantine/dates/styles.css' + +interface DatePickerInputProps extends MantineDatePickerInputProps<'default'> {} +interface DatePickerProps extends MantineDatePickerProps<'default'> {} + +function DatePickerInput(props: DatePickerInputProps) { + return +} + +function DatePicker(props: DatePickerProps) { + return +} + +export { DatePickerInput, DatePicker } +export type { DatePickerInputProps, DatePickerProps } diff --git a/components/detail_page.md b/components/detail_page.md new file mode 100644 index 0000000..49b1acd --- /dev/null +++ b/components/detail_page.md @@ -0,0 +1,58 @@ +--- +name: detail_page +kind: function +lang: ts +domain: ui +version: "1.0.0" +purity: pure +signature: "detailPage(props: DetailPageProps): ReactElement" +description: "Genera una página de detalle de entidad con header (avatar, badge, back), grid de campos, tabs con contadores y timeline de actividad." +tags: [detail, page, entity, timeline, factory, composition, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react, "@mantine/core", "@tabler/icons-react"] +params: + - name: props + desc: "Configuración de página de detalle: título, avatar, badge, tabs, timeline y campos de metadata" +output: "Componente ReactElement que renderiza página de detalle con header, grid de campos y timeline de actividad" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/detail_page.tsx" +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "" +--- + +## Ejemplo + +```tsx +detailPage({ + title: 'John Doe', + subtitle: 'john@example.com', + badge: Active, + onBack: () => router.back(), + fields: [ + { label: 'Role', value: 'Administrator' }, + { label: 'Created', value: 'Mar 15, 2026' }, + { label: 'Bio', value: 'Full stack developer...', span: 2 }, + ], + tabs: [ + { label: 'Projects', value: 'projects', count: 12, content: }, + { label: 'Activity', value: 'activity', count: 48, content: }, + ], + activeTab: 'projects', + timeline: [ + { id: '1', title: 'Deployed v2.1', timestamp: '2 hours ago', variant: 'success' }, + { id: '2', title: 'Updated settings', timestamp: 'Yesterday' }, + { id: '3', title: 'Created project', timestamp: 'Mar 10, 2026' }, + ], +}) +``` + +## Notas + +Factory completa para páginas de detalle. Combina header con back/avatar/badge, grid de metadata, tabs con badges de conteo, y timeline de actividad con variantes de color semántico. diff --git a/components/detail_page.tsx b/components/detail_page.tsx new file mode 100644 index 0000000..e8d577a --- /dev/null +++ b/components/detail_page.tsx @@ -0,0 +1,137 @@ +import * as React from 'react' +import { Stack, Group, Title, Text, ActionIcon, Box, Tabs, Badge, Timeline, SimpleGrid } from '@mantine/core' +import { IconChevronLeft } from '@tabler/icons-react' + +interface DetailField { + label: string + value: React.ReactNode + span?: 1 | 2 +} + +interface DetailTab { + label: string + value: string + content: React.ReactNode + count?: number +} + +interface TimelineEvent { + id: string + title: string + description?: string + timestamp: string + icon?: React.ReactNode + variant?: 'default' | 'success' | 'warning' | 'error' +} + +interface DetailPageProps { + title: string + subtitle?: string + badge?: React.ReactNode + avatar?: React.ReactNode + actions?: React.ReactNode + onBack?: () => void + fields: DetailField[] + tabs?: DetailTab[] + activeTab?: string + onTabChange?: (value: string) => void + timeline?: TimelineEvent[] + className?: string +} + +const variantColors: Record = { + default: 'blue', + success: 'green', + warning: 'yellow', + error: 'red', +} + +export function detailPage({ + title, subtitle, badge, avatar, actions, onBack, + fields, tabs, activeTab, onTabChange, timeline, +}: DetailPageProps): React.ReactElement { + return ( + + {/* Header */} + + + {onBack && ( + + + + )} + {avatar && ( + + {avatar} + + )} + + + {title} + {badge} + + {subtitle && {subtitle}} + + + {actions && {actions}} + + + {/* Fields grid */} + + {fields.map((field, i) => ( + + + {field.label} + {field.value} + + + ))} + + + {/* Tabs */} + {tabs && tabs.length > 0 && ( + + v && onTabChange?.(v)}> + + {tabs.map((tab) => ( + {tab.count} : undefined} + > + {tab.label} + + ))} + + + {tabs.find(t => t.value === activeTab)?.content} + + )} + + {/* Timeline */} + {timeline && timeline.length > 0 && ( + + Activity + + {timeline.map((event) => ( + {event.title}} + > + {event.description && {event.description}} + {event.timestamp} + + ))} + + + )} + + ) +} + +export type { DetailPageProps, DetailField, DetailTab, TimelineEvent } diff --git a/components/dialog.md b/components/dialog.md new file mode 100644 index 0000000..1d7a0bf --- /dev/null +++ b/components/dialog.md @@ -0,0 +1,56 @@ +--- +name: dialog +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Dialog(props: DialogRootProps): JSX.Element" +description: "Diálogo modal accesible con close button y sistema de slots (header, footer, title, description). Mantine Modal." +tags: [dialog, modal, overlay, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", react] +output: "Componente Dialog que renderiza modal accesible con focus trap y sistema de slots composables via Mantine Modal" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/dialog.tsx" +props: + - name: showCloseButton + type: "boolean" + required: false + description: "Mostrar botón de cerrar (default true)" +emits: [onOpenChange] +has_state: true +framework: react +variant: [default] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/dialog.tsx" +--- + +## Ejemplo + +```tsx + + + + + Título + Descripción + +

    Contenido

    + + + +
    +
    +``` + +## Notas + +10 subcomponentes exportados. Mantine Modal para accesibilidad completa (focus trap, escape, click outside). DialogPortal y DialogOverlay son no-ops mantenidos por compatibilidad. diff --git a/components/dialog.tsx b/components/dialog.tsx new file mode 100644 index 0000000..ef22cde --- /dev/null +++ b/components/dialog.tsx @@ -0,0 +1,134 @@ +import * as React from 'react' +import { Modal, Box, Text, Group } from '@mantine/core' + +interface DialogProps { + open?: boolean + onOpenChange?: (open: boolean) => void + children: React.ReactNode +} + +const DialogContext = React.createContext<{ + open: boolean + setOpen: (open: boolean) => void +}>({ open: false, setOpen: () => {} }) + +function Dialog({ open: controlledOpen, onOpenChange, children }: DialogProps) { + const [internalOpen, setInternalOpen] = React.useState(false) + const open = controlledOpen ?? internalOpen + const setOpen = React.useCallback( + (v: boolean) => { + onOpenChange?.(v) + if (controlledOpen === undefined) setInternalOpen(v) + }, + [controlledOpen, onOpenChange], + ) + return ( + + {children} + + ) +} + +function DialogTrigger({ children, ...props }: React.ComponentProps<'button'>) { + const { setOpen } = React.useContext(DialogContext) + return ( + + ) +} + +function DialogPortal({ children }: { children: React.ReactNode }) { + return <>{children} +} + +function DialogClose({ children, ...props }: React.ComponentProps<'button'>) { + const { setOpen } = React.useContext(DialogContext) + return ( + + ) +} + +function DialogOverlay() { + return null +} + +function DialogContent({ + children, + showCloseButton = true, + className, + ...props +}: React.ComponentProps<'div'> & { showCloseButton?: boolean }) { + const { open, setOpen } = React.useContext(DialogContext) + return ( + setOpen(false)} + withCloseButton={showCloseButton} + radius="md" + padding="md" + size="sm" + centered + data-slot="dialog-content" + className={className} + {...props} + > + {children} + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return +} + +function DialogFooter({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( + + {children} + + ) +} + +function DialogTitle({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( + + {children} + + ) +} + +function DialogDescription({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( + + {children} + + ) +} + +export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } diff --git a/components/dropdown_menu.md b/components/dropdown_menu.md new file mode 100644 index 0000000..4f0be74 --- /dev/null +++ b/components/dropdown_menu.md @@ -0,0 +1,74 @@ +--- +name: dropdown_menu +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "DropdownMenu(props: DropdownMenuProps): JSX.Element" +description: "Menu de acciones y contexto accesible con items, checkboxes, radios, separadores y submenus. Base-UI Menu primitive." +tags: [dropdown, menu, component, ui, interactive, overlay, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente DropdownMenu que renderiza menú desplegable accesible con items, checkboxes, radios y submenus" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/dropdown_menu.tsx" +props: + - name: open + type: "boolean" + required: false + description: "Estado controlado de apertura" + - name: defaultOpen + type: "boolean" + required: false + description: "Estado inicial de apertura" + - name: onOpenChange + type: "(open: boolean) => void" + required: false + description: "Callback cuando cambia el estado" + - name: modal + type: "boolean" + required: false + description: "Comportamiento modal (default: true)" +emits: [onOpenChange] +has_state: false +framework: react +variant: [] +--- + +## Ejemplo + +```tsx + + + + + + Mi cuenta + + console.log("Perfil")}> + Perfil + + + Marcadores + + + + Mas opciones + + Opcion A + + + + +``` + +## Notas + +Exports: DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuPortal. diff --git a/components/dropdown_menu.tsx b/components/dropdown_menu.tsx new file mode 100644 index 0000000..5b755af --- /dev/null +++ b/components/dropdown_menu.tsx @@ -0,0 +1,139 @@ +import * as React from 'react' +import { Menu, Text } from '@mantine/core' + +function DropdownMenu({ children, ...props }: { children: React.ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; modal?: boolean }) { + return ( + + {children} + + ) +} + +function DropdownMenuTrigger({ children, ...props }: { children: React.ReactNode; asChild?: boolean; className?: string }) { + return {children} +} + +function DropdownMenuPortal({ children }: { children?: React.ReactNode }) { + return <>{children} +} + +function DropdownMenuContent({ children, className }: { children?: React.ReactNode; className?: string; sideOffset?: number }) { + return {children} +} + +function DropdownMenuItem({ children, className, inset, ...props }: { + children?: React.ReactNode + className?: string + inset?: boolean + onClick?: () => void + onActivate?: () => void + disabled?: boolean +}) { + return ( + + {children} + + ) +} + +function DropdownMenuCheckboxItem({ children, className, checked, onCheckedChange, ...props }: { + children?: React.ReactNode + className?: string + checked?: boolean + onCheckedChange?: (checked: boolean) => void + disabled?: boolean +}) { + return ( + onCheckedChange?.(!checked)} + disabled={props.disabled} + leftSection={checked ? : } + > + {children} + + ) +} + +function DropdownMenuRadioGroup({ children }: { children?: React.ReactNode; value?: string; onValueChange?: (value: string) => void }) { + return <>{children} +} + +function DropdownMenuRadioItem({ children, className, value, ...props }: { + children?: React.ReactNode + className?: string + value?: string + disabled?: boolean + onClick?: () => void +}) { + return ( + + {children} + + ) +} + +function DropdownMenuGroup({ children }: { children?: React.ReactNode }) { + return <>{children} +} + +function DropdownMenuLabel({ children, className, inset }: { children?: React.ReactNode; className?: string; inset?: boolean }) { + return ( + + {children} + + ) +} + +function DropdownMenuSeparator({ className }: { className?: string }) { + return +} + +function DropdownMenuShortcut({ children, className }: { children?: React.ReactNode; className?: string }) { + return {children} +} + +function DropdownMenuSub({ children }: { children?: React.ReactNode }) { + return <>{children} +} + +function DropdownMenuSubTrigger({ children, className, inset }: { children?: React.ReactNode; className?: string; inset?: boolean }) { + return ( + + {children} + + ) +} + +function DropdownMenuSubContent({ children, className }: { children?: React.ReactNode; className?: string }) { + return {children} +} + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} diff --git a/components/dropzone.md b/components/dropzone.md new file mode 100644 index 0000000..a3e8037 --- /dev/null +++ b/components/dropzone.md @@ -0,0 +1,118 @@ +--- +name: dropzone +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Dropzone(props: DropzoneProps): JSX.Element" +description: "Zona de drag-and-drop para archivos con estados idle/accept/reject, límite de tamaño y tipos MIME. Wrapper sobre Mantine Dropzone." +tags: [dropzone, upload, drag-drop, file, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/dropzone"] +output: "Componente Dropzone que renderiza área de arrastrar y soltar archivos con feedback visual" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/dropzone.tsx" +props: + - name: onDrop + type: "(files: File[]) => void" + required: false + description: "Callback ejecutado cuando el usuario suelta archivos aceptados" + - name: onReject + type: "(files: FileRejection[]) => void" + required: false + description: "Callback ejecutado cuando el usuario suelta archivos rechazados (tipo o tamaño inválido)" + - name: accept + type: "Record" + required: false + description: "Tipos MIME aceptados. Usar IMAGE_MIME_TYPE o MIME_TYPES para constantes predefinidas" + - name: maxSize + type: "number" + required: false + description: "Tamaño máximo de archivo en bytes" + - name: multiple + type: "boolean" + required: false + description: "Permite seleccionar múltiples archivos a la vez" + - name: loading + type: "boolean" + required: false + description: "Muestra estado de carga y desactiva la interacción" + - name: disabled + type: "boolean" + required: false + description: "Desactiva el dropzone" + - name: children + type: "React.ReactNode" + required: true + description: "Contenido interno, generalmente compuesto con DropzoneAccept, DropzoneReject y DropzoneIdle" +emits: [onDrop, onReject] +has_state: true +framework: react +variant: [] +--- + +## Ejemplo + +```tsx +import { + Dropzone, + DropzoneAccept, + DropzoneReject, + DropzoneIdle, + IMAGE_MIME_TYPE, +} from '@fn_library/dropzone' +import { IconPhoto, IconUpload, IconX } from '@tabler/icons-react' +import { Group, Text } from '@mantine/core' + +function ImageUploader() { + return ( + console.log('Archivos aceptados:', files)} + onReject={(files) => console.log('Archivos rechazados:', files)} + accept={IMAGE_MIME_TYPE} + maxSize={5 * 1024 ** 2} + > + + + + + + + + + + +
    + + Arrastra imágenes aquí o haz clic para seleccionar + + + Máximo 5 MB por imagen + +
    +
    +
    + ) +} +``` + +## Notas + +El prop `onDrop` tiene un default vacío (`() => {}`) para que el componente sea válido sin handler. Siempre sobreescribirlo en uso real. + +Sub-componentes exportados: +- `DropzoneAccept` — visible cuando el archivo arrastrado es aceptado (tipo y tamaño válidos) +- `DropzoneReject` — visible cuando el archivo es rechazado +- `DropzoneIdle` — visible en estado de reposo +- `DropzoneFullScreen` — captura drops en cualquier parte de la pantalla + +Constantes de tipos MIME exportadas: +- `IMAGE_MIME_TYPE` — imágenes comunes (png, jpg, gif, webp, etc.) +- `MIME_TYPES` — objeto con claves por tipo (pdf, csv, xlsx, mp4, etc.) diff --git a/components/dropzone.tsx b/components/dropzone.tsx new file mode 100644 index 0000000..a46eede --- /dev/null +++ b/components/dropzone.tsx @@ -0,0 +1,20 @@ +import { Dropzone as MantineDropzone, IMAGE_MIME_TYPE, MIME_TYPES } from '@mantine/dropzone' +import type { DropzoneProps as MantineDropzoneProps } from '@mantine/dropzone' +import '@mantine/dropzone/styles.css' + +interface DropzoneProps extends Partial { + children: React.ReactNode +} + +function Dropzone({ children, ...props }: DropzoneProps) { + return {}} {...props}>{children} +} + +// Re-export sub-components and constants +const DropzoneAccept = MantineDropzone.Accept +const DropzoneReject = MantineDropzone.Reject +const DropzoneIdle = MantineDropzone.Idle +const DropzoneFullScreen = MantineDropzone.FullScreen + +export { Dropzone, DropzoneAccept, DropzoneReject, DropzoneIdle, DropzoneFullScreen, IMAGE_MIME_TYPE, MIME_TYPES } +export type { DropzoneProps } diff --git a/components/empty_state.md b/components/empty_state.md new file mode 100644 index 0000000..5f8fd70 --- /dev/null +++ b/components/empty_state.md @@ -0,0 +1,103 @@ +--- +name: empty_state +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "EmptyState(props: EmptyStateProps): JSX.Element" +description: "Placeholder para listas y tablas vacías con icono, título, descripción y acción opcional. Tabler Icons por defecto." +tags: [empty-state, placeholder, no-data, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", "@tabler/icons-react"] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/empty_state.tsx" +framework: react +has_state: false +emits: [onAction] +props: + - name: icon + type: "React.ReactNode" + required: false + description: "Icono a mostrar. Default: IconInbox de @tabler/icons-react." + - name: title + type: "string" + required: false + description: "Título del empty state. Default: 'No data found'." + - name: description + type: "string" + required: false + description: "Descripción explicativa. Default: 'There are no items to display yet.'." + - name: actionLabel + type: "string" + required: false + description: "Texto del botón de acción. Se muestra solo si también hay onAction." + - name: onAction + type: "() => void" + required: false + description: "Callback del botón de acción. Se muestra solo si también hay actionLabel." + - name: size + type: "MantineSize" + required: false + description: "Tamaño general del componente. Afecta el icono, texto y botón. Default: 'md'." + - name: children + type: "React.ReactNode" + required: false + description: "Contenido custom renderizado debajo de la descripción y antes del botón." +output: "Componente EmptyState centrado con icono, mensaje y botón de acción para estados sin datos" +params: + - name: props + desc: "Props del componente EmptyState" +--- + +## Ejemplo + +```tsx +import { EmptyState } from '@fn_library/empty_state' + +// Default — sin datos + + +// Con acción + navigate('/new')} +/> + +// Con icono custom +import { IconDatabase } from '@tabler/icons-react' + +} + title="No databases connected" + description="Connect a database to start querying data." + size="lg" +/> + +// Dentro de una Card +import { Card } from '@mantine/core' +import { EmptyState } from '@fn_library/empty_state' + + + + +``` + +## Notas + +El tamaño del icono escala con `size`: xs=32, sm=40, md=48, lg=64, xl=80. +El orden del heading (`Title order`) es 5 para xs/sm y 4 para md/lg/xl. +El botón usa `variant="light"` de Mantine — hereda el color primario del tema. +`children` se renderiza entre la descripción y el botón, útil para filtros o links adicionales. diff --git a/components/empty_state.tsx b/components/empty_state.tsx new file mode 100644 index 0000000..b57b176 --- /dev/null +++ b/components/empty_state.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' +import { Button, Stack, Text, Title, type MantineSize } from '@mantine/core' +import { IconInbox } from '@tabler/icons-react' + +interface EmptyStateProps { + /** Icono a mostrar (default: IconInbox) */ + icon?: React.ReactNode + /** Título */ + title?: string + /** Descripción */ + description?: string + /** Texto del botón de acción */ + actionLabel?: string + /** Callback del botón */ + onAction?: () => void + /** Tamaño general */ + size?: MantineSize + /** Contenido custom debajo de la descripción */ + children?: React.ReactNode +} + +function EmptyState({ + icon, + title = 'No data found', + description = 'There are no items to display yet.', + actionLabel, + onAction, + size = 'md', + children, +}: EmptyStateProps) { + const iconSize = size === 'xs' ? 32 : size === 'sm' ? 40 : size === 'lg' ? 64 : size === 'xl' ? 80 : 48 + + return ( + + + {icon || } + + + {title} + + + {description} + + {children} + {actionLabel && onAction && ( + + )} + + ) +} + +export { EmptyState } +export type { EmptyStateProps } diff --git a/components/error_page.md b/components/error_page.md new file mode 100644 index 0000000..8ba4b70 --- /dev/null +++ b/components/error_page.md @@ -0,0 +1,75 @@ +--- +name: error_page +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "ErrorPage(config: ErrorPageConfig): JSX.Element" +description: "Genera página de error con código grande, título, descripción y acciones. Soporta 404, 500, 403 y cualquier código custom." +tags: [error, 404, 500, page, empty-state, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react, "@mantine/core"] +params: + - name: code + desc: "Código de error numérico o string a mostrar prominentemente (404, 500, 403, o cualquier valor custom)" + - name: title + desc: "Título del error mostrado bajo el código. Default: 'Page not found'" + - name: description + desc: "Descripción explicativa del error. Default: mensaje genérico de página no encontrada" + - name: actionLabel + desc: "Texto del botón de acción principal. Default: 'Go back to home'" + - name: onAction + desc: "Callback invocado al pulsar el botón de acción principal" + - name: extraActions + desc: "Nodos React adicionales renderizados junto al botón principal (botones secundarios, links, etc.)" +output: "Página de error centrada con código prominente, mensaje descriptivo y botones de acción" +has_state: false +framework: react +emits: [onAction] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/error_page.tsx" +--- + +## Ejemplo + +```tsx +import { ErrorPage } from '@fn_library/error_page' +import { Button } from '@mantine/core' + +// 404 con defaults + navigate('/')} /> + +// 500 custom + window.location.reload()} +/> + +// 403 con acciones extra + navigate('/dashboard')} + extraActions={ + + } +/> +``` + +## Notas + +El código de error se muestra con `fz={120}` y opacidad reducida (0.25) para crear un efecto visual de fondo sin distraer del mensaje. Acepta `number | string` para soportar códigos custom como "503" o "Maintenance". diff --git a/components/error_page.tsx b/components/error_page.tsx new file mode 100644 index 0000000..b93dbb4 --- /dev/null +++ b/components/error_page.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { Button, Container, Group, Stack, Text, Title } from '@mantine/core' + +interface ErrorPageConfig { + /** Código de error (404, 500, 403, etc.) */ + code?: number | string + /** Título del error */ + title?: string + /** Descripción del error */ + description?: string + /** Texto del botón de acción */ + actionLabel?: string + /** Callback del botón */ + onAction?: () => void + /** Acciones extra además del botón principal */ + extraActions?: React.ReactNode +} + +function ErrorPage({ + code = 404, + title = 'Page not found', + description = 'The page you are looking for does not exist. You may have mistyped the address, or the page has been moved to another URL.', + actionLabel = 'Go back to home', + onAction, + extraActions, +}: ErrorPageConfig) { + return ( + + + + {code} + + + {title} + + + {description} + + + + {extraActions} + + + + ) +} + +export { ErrorPage } +export type { ErrorPageConfig } diff --git a/components/file_input.md b/components/file_input.md new file mode 100644 index 0000000..1795a80 --- /dev/null +++ b/components/file_input.md @@ -0,0 +1,102 @@ +--- +name: file_input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FileInput(props: FileInputProps): JSX.Element" +description: "Input de archivos con soporte para múltiples archivos, tipos aceptados y botón de limpiar. Wrapper sobre Mantine FileInput." +tags: [file, upload, input, form, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/file_input.tsx" +framework: react +has_state: true +emits: [onChange] +props: + - name: multiple + type: "boolean" + required: false + description: "Permite seleccionar múltiples archivos" + - name: accept + type: "string" + required: false + description: "Tipos MIME o extensiones aceptadas (ej: 'image/*', '.pdf,.docx')" + - name: clearable + type: "boolean" + required: false + description: "Muestra botón para limpiar el archivo seleccionado" + - name: value + type: "File | File[] | null" + required: false + description: "Valor controlado del input" + - name: onChange + type: "(value: File | File[] | null) => void" + required: false + description: "Callback que se dispara al seleccionar o limpiar un archivo" + - name: placeholder + type: "string" + required: false + description: "Texto mostrado cuando no hay archivo seleccionado" + - name: label + type: "string" + required: false + description: "Etiqueta visible sobre el input" + - name: disabled + type: "boolean" + required: false + description: "Deshabilita el input" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño del componente" +params: + - name: props + desc: "Props de FileInput: archivo(s) seleccionado(s), tipos aceptados, modo múltiple, estado controlado y apariencia" +output: "Componente FileInput que renderiza input para selección de archivos con preview del nombre" +--- + +## Ejemplo + +```tsx +import { FileInput } from '@fn_library' + +// Archivo único + + +// Múltiples archivos + + +// Solo imágenes + +``` + +## Notas + +Wrapper directo sobre `FileInput` de `@mantine/core`. Acepta todas las props de Mantine sin restricciones. + +Para `multiple: true`, el tipo de `value` y `onChange` cambia a `File[] | null` automáticamente gracias al tipado genérico de Mantine. + +El prop `clearable` añade un ícono de X que permite vaciar la selección sin reabrir el explorador de archivos. diff --git a/components/file_input.tsx b/components/file_input.tsx new file mode 100644 index 0000000..38a551d --- /dev/null +++ b/components/file_input.tsx @@ -0,0 +1,10 @@ +import { FileInput as MantineFileInput, type FileInputProps as MantineFileInputProps } from '@mantine/core' + +interface FileInputProps extends MantineFileInputProps {} + +function FileInput(props: FileInputProps) { + return +} + +export { FileInput } +export type { FileInputProps } diff --git a/components/form_field.md b/components/form_field.md new file mode 100644 index 0000000..3c4670a --- /dev/null +++ b/components/form_field.md @@ -0,0 +1,54 @@ +--- +name: form_field +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FormField(props: FormFieldProps): JSX.Element" +description: "Wrapper de campo de formulario con label, helper text, error y ARIA automáticos. Inyecta id y aria-describedby a hijos." +tags: [form, field, label, error, component, ui, accessibility] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente FormField que renderiza wrapper de campo con label, helper text, error y ARIA automáticos" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/form_field.tsx" +props: + - name: label + type: "string" + required: false + description: "Texto del label" + - name: helperText + type: "string" + required: false + description: "Texto de ayuda" + - name: error + type: "string" + required: false + description: "Mensaje de error (reemplaza helperText)" + - name: children + type: "ReactNode" + required: true + description: "Input o componente de formulario" +emits: [] +has_state: false +framework: react +variant: [default] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/form-field.tsx" +--- + +## Ejemplo + +```tsx + + + +``` diff --git a/components/form_field.tsx b/components/form_field.tsx new file mode 100644 index 0000000..0504161 --- /dev/null +++ b/components/form_field.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' +import { Box, Text } from '@mantine/core' + +interface FormFieldProps { + label?: string + helperText?: string + error?: string + children: React.ReactNode + className?: string +} + +function FormField({ label, helperText, error, children, className }: FormFieldProps) { + const id = React.useId() + const inputId = `${id}-input` + const helperId = `${id}-helper` + const errorId = `${id}-error` + + const describedBy = [helperText ? helperId : null, error ? errorId : null].filter(Boolean).join(' ') || undefined + + const childWithProps = React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child as React.ReactElement>, { + id: inputId, + 'aria-invalid': error ? true : undefined, + 'aria-describedby': describedBy, + error: error || undefined, + }) + } + return child + }) + + return ( + + {label && ( + + {label} + + )} + {childWithProps} + {helperText && !error && ( + + {helperText} + + )} + {error && ( + + {error} + + )} + + ) +} + +export { FormField } +export type { FormFieldProps } diff --git a/components/graph/graph_container.md b/components/graph/graph_container.md new file mode 100644 index 0000000..7e47892 --- /dev/null +++ b/components/graph/graph_container.md @@ -0,0 +1,104 @@ +--- +name: graph_container +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "GraphContainer(props: GraphContainerProps): JSX.Element" +description: "Interactive graph visualization with sigma.js, graphology, and ForceAtlas2 layout" +tags: [component, ui, graph, visualization, sigma, graphology, network] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["graphology", "graphology-layout-forceatlas2", "sigma"] +output: "Componente GraphContainer que renderiza grafo interactivo con sigma.js, ForceAtlas2 layout y legend" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/graph/index.tsx" +props: + - name: data + type: "GraphData" + required: true + description: "Graph data with nodes and edges arrays" + - name: layout + type: "'organic' | 'random'" + required: false + description: "Layout algorithm (default: organic/ForceAtlas2)" + - name: showLegend + type: "boolean" + required: false + description: "Show node type legend overlay" + - name: nodeTypes + type: "NodeType[]" + required: false + description: "Node type definitions for legend" + - name: onNodeClick + type: "(node: GraphNode) => void" + required: false + description: "Node click handler" + - name: onNodeDoubleClick + type: "(node: GraphNode) => void" + required: false + description: "Node double-click handler" + - name: theme + type: "GraphTheme" + required: false + description: "Visual theme overrides" + - name: height + type: "string | number" + required: false + description: "Container height (default: 100%)" + - name: className + type: "string" + required: false + description: "Additional CSS classes" +emits: [] +has_state: true +framework: react +variant: [] +--- + +## Ejemplo + +```tsx +import { GraphContainer } from '@fn_library/graph' +import type { GraphData } from '@fn_library/graph' + +const data: GraphData = { + nodes: [ + { id: '1', label: 'Node A', color: '#e74c3c', size: 10 }, + { id: '2', label: 'Node B', color: '#3498db', size: 8 }, + ], + edges: [ + { id: 'e1', source: '1', target: '2', label: 'connects', type: 'arrow' }, + ], +} + +function MyGraph() { + return ( + console.log('clicked:', node.id)} + height="500px" + /> + ) +} +``` + +## Notas + +- Usa graphology como estructura de datos de grafo +- ForceAtlas2 para layout organico (iterations adaptativas segun numero de nodos) +- Sigma.js para renderizado WebGL de alto rendimiento +- Soporta grafos dirigidos multi-edge +- El componente limpia la instancia Sigma al desmontar diff --git a/components/graph/index.tsx b/components/graph/index.tsx new file mode 100644 index 0000000..baac506 --- /dev/null +++ b/components/graph/index.tsx @@ -0,0 +1,283 @@ +import * as React from "react" +import Graph from "graphology" +import forceAtlas2 from "graphology-layout-forceatlas2" +import { Sigma } from "sigma" + +// ── Types ───────────────────────────────────────────────────────────────── + +export interface GraphNode { + id: string + label: string + type?: string + color?: string + size?: number + x?: number + y?: number + [key: string]: unknown +} + +export interface GraphEdge { + id: string + source: string + target: string + label?: string + color?: string + size?: number + type?: "arrow" | "line" + weight?: number + [key: string]: unknown +} + +export interface GraphData { + nodes: GraphNode[] + edges: GraphEdge[] +} + +export interface NodeType { + type: string + color: string + label: string +} + +export interface GraphTheme { + backgroundColor?: string + nodeColor?: string + nodeSize?: number + edgeColor?: string + edgeSize?: number + labelColor?: string + selectionColor?: string +} + +export interface ContextMenuTarget { + type: "node" | "edge" | "canvas" + id?: string + data?: GraphNode | GraphEdge +} + +export interface GraphContainerProps { + data: GraphData + layout?: "organic" | "random" + showToolbar?: boolean + showLegend?: boolean + showMinimap?: boolean + nodeTypes?: NodeType[] + onNodeClick?: (node: GraphNode) => void + onNodeDoubleClick?: (node: GraphNode) => void + onContextMenu?: (event: MouseEvent, target: ContextMenuTarget) => void + enableSelection?: boolean + selectionMode?: "single" | "multiple" + theme?: GraphTheme + height?: string | number + className?: string +} + +const DEFAULT_THEME: Required = { + backgroundColor: "var(--background, #0a0a0f)", + nodeColor: "#95a5a6", + nodeSize: 8, + edgeColor: "rgba(255,255,255,0.19)", + edgeSize: 1, + labelColor: "#e0e0e0", + selectionColor: "#3b82f6", +} + +// ── Component ───────────────────────────────────────────────────────────── + +function GraphContainer({ + data, + layout = "organic", + showLegend = false, + nodeTypes = [], + onNodeClick, + onNodeDoubleClick, + onContextMenu, + theme: themeProp, + height = "100%", + className, +}: GraphContainerProps) { + const containerRef = React.useRef(null) + const sigmaRef = React.useRef(null) + const graphRef = React.useRef(null) + const theme = React.useMemo( + () => ({ ...DEFAULT_THEME, ...themeProp }), + [themeProp], + ) + + // Build + render — wait for container to have dimensions + const [ready, setReady] = React.useState(false) + React.useEffect(() => { + const el = containerRef.current + if (!el) return + if (el.clientHeight > 0 && el.clientWidth > 0) { + setReady(true) + return + } + const ro = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.contentRect.height > 0 && entry.contentRect.width > 0) { + setReady(true) + ro.disconnect() + } + } + }) + ro.observe(el) + return () => ro.disconnect() + }, []) + + React.useEffect(() => { + const el = containerRef.current + if (!el || !ready) return + + // Cleanup previous instance + if (sigmaRef.current) { + sigmaRef.current.kill() + sigmaRef.current = null + } + + const g = new Graph({ multi: true, type: "directed" }) + graphRef.current = g + + // Add nodes — store entity type as entityType to avoid sigma interpreting it as render program + for (const n of data.nodes) { + g.addNode(n.id, { + label: n.label, + x: n.x ?? (Math.random() - 0.5) * 10, + y: n.y ?? (Math.random() - 0.5) * 10, + size: n.size ?? theme.nodeSize, + color: n.color ?? theme.nodeColor, + entityType: n.type, + }) + } + + // Add edges + for (const e of data.edges) { + try { + g.addEdgeWithKey(e.id, e.source, e.target, { + label: e.label, + size: e.size ?? theme.edgeSize, + color: e.color ?? theme.edgeColor, + type: e.type === "arrow" ? "arrow" : "line", + weight: e.weight ?? 1, + }) + } catch { + // skip duplicate keys + } + } + + // Layout + if (layout === "organic" && g.order > 0) { + forceAtlas2.assign(g, { + iterations: Math.min(500, Math.max(100, g.order * 5)), + settings: { + gravity: 1, + scalingRatio: 2, + slowDown: 5, + barnesHutOptimize: g.order > 300, + }, + }) + } + + // Render + const renderer = new Sigma(g, el, { + allowInvalidContainer: true, + renderEdgeLabels: false, + defaultEdgeColor: theme.edgeColor, + defaultNodeColor: theme.nodeColor, + labelColor: { color: theme.labelColor }, + labelSize: 11, + }) + + sigmaRef.current = renderer + + // Events + if (onNodeClick) { + renderer.on("clickNode", ({ node }) => { + const attrs = g.getNodeAttributes(node) + onNodeClick({ id: node, ...attrs } as unknown as GraphNode) + }) + } + if (onNodeDoubleClick) { + renderer.on("doubleClickNode", ({ node }) => { + const attrs = g.getNodeAttributes(node) + onNodeDoubleClick({ id: node, ...attrs } as unknown as GraphNode) + }) + } + if (onContextMenu) { + renderer.on("rightClickNode", ({ node, event }) => { + const mouseEvent = event.original as MouseEvent + mouseEvent.preventDefault() + const attrs = g.getNodeAttributes(node) + onContextMenu(mouseEvent, { + type: "node", + id: node, + data: { id: node, ...attrs } as unknown as GraphNode, + }) + }) + renderer.on("rightClickStage", ({ event }) => { + const mouseEvent = event.original as MouseEvent + mouseEvent.preventDefault() + onContextMenu(mouseEvent, { type: "canvas" }) + }) + } + + return () => { + renderer.kill() + sigmaRef.current = null + graphRef.current = null + } + }, [data, layout, theme, onNodeClick, onNodeDoubleClick, onContextMenu, ready]) + + // Container background + const containerStyle: React.CSSProperties = { + height, + width: "100%", + position: "relative", + background: theme.backgroundColor, + borderRadius: "var(--radius, 0.5rem)", + overflow: "hidden", + } + + return ( +
    +
    + {showLegend && nodeTypes.length > 0 && ( +
    + {nodeTypes.map((nt) => ( +
    + + {nt.label} +
    + ))} +
    + )} +
    + ) +} + +export { GraphContainer } diff --git a/components/index.ts b/components/index.ts new file mode 100644 index 0000000..906cf6c --- /dev/null +++ b/components/index.ts @@ -0,0 +1,136 @@ +// Barrel export — @fn_library +// Primitives +export { Alert, AlertTitle, AlertDescription } from './alert' +export { Badge, badgeVariants } from './badge' +export { Button, buttonVariants } from './button' +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent } from './card' +export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './dialog' +export { Input, InputGroup, InputIcon } from './input' +export { Label } from './label' +export { KPICard } from './kpi_card' +export { Select } from './select' +export type { SelectProps } from './select' +export { SimpleSelect } from './simple_select' +export type { SimpleSelectOption, SimpleSelectGroup, SimpleSelectOptions } from './simple_select' +export { Skeleton, SkeletonAvatar, SkeletonButton, SkeletonCard, SkeletonTable, SkeletonText } from './skeleton' +export { Sparkline } from './sparkline' +export type { SparklineProps, SparklineVariant } from './sparkline' +export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs' +export { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from './tooltip' +export { FormField } from './form_field' +export type { FormFieldProps } from './form_field' +export { PageHeader } from './page_header' +export { ProgressBar } from './progress_bar' + +// Charts +export { AreaChart } from './area_chart' +export type { AreaChartProps, GradientConfig } from './area_chart' +export { BarChart } from './bar_chart' +export type { BarChartProps } from './bar_chart' +export { LineChart } from './line_chart' +export type { LineChartProps, CurveType } from './line_chart' +export { PieChart } from './pie_chart' +export type { PieChartProps } from './pie_chart' +export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, getSeriesColor } from './chart_container' +export type { Series } from './chart_container' + +// Data +export { DataTable } from './data_table' +export type { DataTableProps, ColumnDef } from './data_table' + +// Mantine Provider +export { FnMantineProvider } from './mantine_provider' +export type { FnMantineProviderProps } from './mantine_provider' + +// Page templates +export { analyticsPage } from './analytics_page' +export type { AnalyticsPageProps, MetricConfig, ChartConfig } from './analytics_page' +export { crudPage } from './crud_page' +export type { CrudPageProps, CrudField } from './crud_page' +export { dashboardLayout } from './dashboard_layout' +export type { DashboardWidget, DashboardLayoutProps } from './dashboard_layout' +export { detailPage } from './detail_page' +export type { DetailPageProps, DetailField, DetailTab, TimelineEvent } from './detail_page' +export { settingsPage } from './settings_page' +export type { SettingsPageProps, SettingSection, SettingField } from './settings_page' + +// Hooks — Wails +export { useWailsQuery } from './use_wails_query' +export type { UseWailsQueryOptions, UseWailsQueryResult } from './use_wails_query' +export { useWailsMutation } from './use_wails_mutation' +export type { UseWailsMutationOptions, UseWailsMutationResult } from './use_wails_mutation' +export { useWailsStream, useWailsLogs } from './use_wails_stream' +export type { UseWailsStreamOptions, UseWailsStreamResult } from './use_wails_stream' +export { useWailsEvent, useWailsEmit } from './use_wails_event' +export type { UseWailsEventOptions, UseWailsEventResult } from './use_wails_event' + +// Accordion +export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './accordion' +export type { AccordionProps } from './accordion' + +// Avatar +export { Avatar, avatarVariants } from './avatar' +export type { AvatarProps } from './avatar' + +// Breadcrumb +export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from './breadcrumb' + +// Checkbox +export { Checkbox } from './checkbox' +export type { CheckboxProps } from './checkbox' + +// Command +export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut } from './command' +export type { CommandItemData, CommandProps } from './command' + +// Dropdown Menu +export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from './dropdown_menu' + +// Pagination +export { Pagination } from './pagination' +export type { PaginationProps } from './pagination' + +// Popover +export { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverPortal, PopoverTitle, PopoverTrigger } from './popover' + +// Radio Group +export { RadioGroup, RadioGroupItem } from './radio_group' +export type { RadioGroupItemProps } from './radio_group' + +// Sheet +export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, sheetVariants } from './sheet' +export type { SheetContentProps } from './sheet' + +// Switch +export { SwitchToggle } from './switch_toggle' +export type { SwitchToggleProps } from './switch_toggle' + +// Textarea +export { Textarea } from './textarea' +export type { TextareaProps } from './textarea' + +// Toast +export { Toast, ToastProvider, ToastViewport, toastVariants, useToast } from './toast' +export type { ToastEntry, ToastProps, ToastViewportProps } from './toast' + +// Search +export { SearchBar } from './search_bar' +export type { SearchBarProps } from './search_bar' + +// Hooks — Canvas +export { useAnimatedCanvas } from './use_animated_canvas' + +// Wails Provider +export { WailsProvider } from './wails_provider' + +// New Mantine components +export { FnAppShell } from './app_shell' +export { FnStepper } from './stepper' +export { FnTimeline } from './timeline' +export { FnActionIcon } from './action_icon' +export { FnNumberInput } from './number_input' +export { FnSegmentedControl } from './segmented_control' +export { FnLoadingOverlay } from './loading_overlay' +export { FnRingProgress } from './ring_progress' +export { FnNavLink } from './nav_link' +export { FnIndicator } from './indicator' diff --git a/components/indicator.md b/components/indicator.md new file mode 100644 index 0000000..379f3be --- /dev/null +++ b/components/indicator.md @@ -0,0 +1,77 @@ +--- +name: indicator +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnIndicator(props: FnIndicatorProps): JSX.Element" +description: "Badge indicador posicionado sobre un elemento hijo. Wrapper sobre Mantine Indicator." +tags: [mantine, indicator, badge, notification, dot, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: children + type: "ReactNode" + required: true + description: "Elemento sobre el cual se posiciona el indicador" + - name: color + type: "MantineColor" + required: false + description: "Color del indicador, default red" + - name: size + type: "number" + required: false + description: "Tamano del dot en px, default 10" + - name: position + type: "'top-start' | 'top-center' | 'top-end' | 'middle-start' | 'middle-center' | 'middle-end' | 'bottom-start' | 'bottom-center' | 'bottom-end'" + required: false + description: "Posicion del indicador, default top-end" + - name: processing + type: "boolean" + required: false + description: "Animacion de pulso, default false" + - name: disabled + type: "boolean" + required: false + description: "Oculta el indicador, default false" + - name: label + type: "ReactNode" + required: false + description: "Contenido dentro del indicador (numero, texto)" +output: "Elemento hijo con un dot/badge indicador posicionado en una esquina" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/indicator.tsx" +emits: [] +has_state: false +variant: [] +--- + +## Ejemplo + +```tsx +import { FnIndicator } from '@fn_library' +import { FnActionIcon } from '@fn_library' +import { IconBell } from '@tabler/icons-react' + +{/* Dot simple */} + + } /> + + +{/* Con contador */} + + + +``` + +## Notas + +Wrapper sobre Mantine `Indicator`. El `processing` prop agrega una animacion de pulso al dot. Si se provee `label`, el indicador se agranda para mostrar contenido. `disabled` oculta el indicador sin desmontar el componente. diff --git a/components/indicator.tsx b/components/indicator.tsx new file mode 100644 index 0000000..13d130c --- /dev/null +++ b/components/indicator.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { Indicator } from '@mantine/core' +import type { MantineColor } from '@mantine/core' + +interface FnIndicatorProps { + children: React.ReactNode + color?: MantineColor + size?: number + position?: 'top-start' | 'top-center' | 'top-end' | 'middle-start' | 'middle-center' | 'middle-end' | 'bottom-start' | 'bottom-center' | 'bottom-end' + processing?: boolean + disabled?: boolean + label?: React.ReactNode +} + +function FnIndicator({ + children, + color = 'red', + size = 10, + position = 'top-end', + processing = false, + disabled = false, + label, +}: FnIndicatorProps) { + return ( + + {children} + + ) +} + +export { FnIndicator } +export type { FnIndicatorProps } diff --git a/components/input.md b/components/input.md new file mode 100644 index 0000000..b0ad72f --- /dev/null +++ b/components/input.md @@ -0,0 +1,52 @@ +--- +name: input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Input(props: InputHTMLAttributes): JSX.Element" +description: "Campo de entrada accesible con soporte para iconos, grupos, validación ARIA y estados disabled/invalid. Mantine TextInput." +tags: [input, form, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Input que renderiza campo de entrada accesible con soporte para iconos y validación ARIA" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/input.tsx" +props: + - name: type + type: "string" + required: false + description: "Tipo de input HTML" + - name: className + type: "string" + required: false + description: "Clases CSS adicionales" +emits: [onChange, onFocus, onBlur] +has_state: false +framework: react +variant: [default] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/input.tsx" +--- + +## Ejemplo + +```tsx + + + + + +``` + +## Notas + +Exporta Input, InputGroup e InputIcon. Usa Mantine TextInput internamente. InputGroup e InputIcon se mantienen como wrappers de compatibilidad — para nuevos usos preferir leftSection/rightSection de Mantine TextInput directamente. diff --git a/components/input.tsx b/components/input.tsx new file mode 100644 index 0000000..1e45468 --- /dev/null +++ b/components/input.tsx @@ -0,0 +1,57 @@ +import * as React from 'react' +import { TextInput, Box } from '@mantine/core' + +function Input({ + className, + type, + ...props +}: React.ComponentProps & { type?: string }) { + return ( + + ) +} + +interface InputGroupProps { + children: React.ReactNode + className?: string +} + +function InputGroup({ children, className }: InputGroupProps) { + return ( + + {children} + + ) +} + +interface InputIconProps { + children: React.ReactNode + position: 'start' | 'end' + className?: string +} + +function InputIcon({ children, position, className }: InputIconProps) { + return ( + + {children} + + ) +} + +export { Input, InputGroup, InputIcon } +export type { InputGroupProps, InputIconProps } diff --git a/components/kpi_card.md b/components/kpi_card.md new file mode 100644 index 0000000..ef94bf3 --- /dev/null +++ b/components/kpi_card.md @@ -0,0 +1,94 @@ +--- +name: kpi_card +kind: component +lang: ts +domain: ui +version: "2.0.0" +purity: impure +signature: "KPICard(props: KPICardProps): JSX.Element" +description: "Card de KPI con label, valor+unidad, delta descriptivo con color semántico, icono, slot de chart inline y action. 3 tamaños." +tags: [kpi, card, metrics, dashboard, component, ui, sparkline] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react] +output: "Componente KPICard que renderiza card de métrica con label, valor, delta descriptivo, icono y slot de mini chart" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/kpi_card.tsx" +props: + - name: label + type: "string" + required: true + description: "Etiqueta del KPI" + - name: value + type: "string | number" + required: true + description: "Valor principal" + - name: unit + type: "string" + required: false + description: "Unidad junto al valor en font menor (ej: k, ms, %)" + - name: delta + type: "{ value: number; isPositive: boolean; label?: string; suffix?: string }" + required: false + description: "Cambio con dirección, label descriptivo y sufijo" + - name: icon + type: "ReactNode" + required: false + description: "Icono a la izquierda del label" + - name: action + type: "ReactNode" + required: false + description: "Slot top-right para menú o acciones" + - name: chart + type: "ReactNode" + required: false + description: "Slot para mini chart inline junto al valor" + - name: size + type: "'sm' | 'default' | 'lg'" + required: false + description: "Tamaño" +emits: [] +has_state: false +framework: react +variant: [sm, default, lg] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/kpi-card.tsx" +--- + +## Ejemplo + +```tsx +import { KPICard, Sparkline } from '@fn_library' + +{/* Básico */} + + +{/* Con unidad separada, delta descriptivo, y mini barras */} +} + delta={{ value: 15, isPositive: true, label: "Prompts Increased by", suffix: "vs yesterday" }} + chart={} + action={} +/> + +{/* Dashboard dark sin bordes */} + +``` + +## Notas + +- El icono ahora se renderiza a la **izquierda** del label (antes estaba a la derecha). +- `unit` separa la unidad del valor con font menor para el efecto "124 k" del diseño. +- `delta.label` y `delta.suffix` permiten texto descriptivo: "Increased by ▲ +15% vs yesterday". +- `chart` es un slot genérico — pasar un `` para mini barras multicolor. +- `action` es un slot top-right para menú contextual. +- Usa `cn()` para merge de clases. `className="border-0 shadow-none"` para dashboards dark. diff --git a/components/kpi_card.tsx b/components/kpi_card.tsx new file mode 100644 index 0000000..7c31d0b --- /dev/null +++ b/components/kpi_card.tsx @@ -0,0 +1,91 @@ +import * as React from 'react' +import { Paper, Text, Group, Stack, Box } from '@mantine/core' + +type KPICardSize = 'sm' | 'default' | 'lg' + +interface Delta { + value: number + isPositive: boolean + label?: string + suffix?: string +} + +interface KPICardProps extends React.HTMLAttributes { + label: string + value: string | number + unit?: string + delta?: Delta + icon?: React.ReactNode + action?: React.ReactNode + chart?: React.ReactNode + subtitle?: string + size?: KPICardSize +} + +const valueSizes: Record = { + sm: '1.5rem', + default: '1.875rem', + lg: '2.25rem', +} + +const unitSizes: Record = { + sm: 'md', + default: 'lg', + lg: 'xl', +} + +const labelSizes: Record = { + sm: 'xs', + default: 'sm', + lg: 'md', +} + +const KPICard = React.forwardRef( + ({ label, value, unit, delta, icon, action, chart, subtitle, size = 'default', className, ...props }, ref) => { + const deltaColor = delta + ? delta.value === 0 ? 'dimmed' + : delta.isPositive ? 'teal' + : 'red' + : undefined + + return ( + + + + {icon && {icon}} + + {label} + {subtitle && {subtitle}} + + + {action && {action}} + + + + + + + {value} + + {unit && {unit}} + + {delta && ( + + {delta.label && {delta.label}} + + {delta.isPositive ? '\u25B2' : '\u25BC'} {delta.value > 0 ? '+' : ''}{delta.value}{delta.label ? '' : '%'} + + {delta.suffix && {delta.suffix}} + + )} + + {chart && {chart}} + + + ) + } +) +KPICard.displayName = 'KPICard' + +export { KPICard } +export type { KPICardProps, Delta, KPICardSize } diff --git a/components/label.md b/components/label.md new file mode 100644 index 0000000..08cad4b --- /dev/null +++ b/components/label.md @@ -0,0 +1,40 @@ +--- +name: label +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Label(props: LabelHTMLAttributes): JSX.Element" +description: "Etiqueta de formulario accesible con soporte para estados disabled. Mantine Text con component=label." +tags: [label, form, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Label que renderiza etiqueta de formulario accesible con soporte para estados disabled" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/label.tsx" +props: + - name: className + type: "string" + required: false + description: "Clases CSS adicionales" +emits: [] +has_state: false +framework: react +variant: [default] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/label.tsx" +--- + +## Ejemplo + +```tsx + +``` diff --git a/components/label.tsx b/components/label.tsx new file mode 100644 index 0000000..0ab0f33 --- /dev/null +++ b/components/label.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' +import { Text } from '@mantine/core' + +function Label({ className, ...props }: React.ComponentProps<'label'>) { + return ( + + ) +} + +export { Label } diff --git a/components/line_chart.md b/components/line_chart.md new file mode 100644 index 0000000..0c0ee63 --- /dev/null +++ b/components/line_chart.md @@ -0,0 +1,65 @@ +--- +name: line_chart +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "LineChart(props: LineChartProps): JSX.Element" +description: "Gráfico de líneas @mantine/charts con multi-series, 5 tipos de curva, líneas de referencia y tooltips." +tags: [chart, line, visualization, mantine, component, ui] +uses_functions: [chart_container_ts_ui] +uses_types: [ChartSeries_ts_ui] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/charts", "@mantine/core"] +output: "Componente LineChart que renderiza gráfico de líneas multi-series con curvas customizables y líneas de referencia" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/line_chart.tsx" +props: + - name: data + type: "Record[]" + required: true + description: "Array de datos" + - name: xKey + type: "string" + required: true + description: "Key del eje X" + - name: series + type: "Series[]" + required: false + description: "Series de datos" + - name: curveType + type: "'linear' | 'monotone' | 'step' | 'stepBefore' | 'stepAfter'" + required: false + description: "Tipo de curva (default monotone)" + - name: referenceLines + type: "Array<{ y: number; label?: string; color?: string }>" + required: false + description: "Líneas de referencia horizontales" +emits: [] +has_state: false +framework: react +variant: [default] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/charts/line-chart.tsx" +--- + +## Ejemplo + +```tsx + +``` diff --git a/components/line_chart.tsx b/components/line_chart.tsx new file mode 100644 index 0000000..29eef87 --- /dev/null +++ b/components/line_chart.tsx @@ -0,0 +1,60 @@ +import { LineChart as MantineLineChart } from '@mantine/charts' +import { Paper } from '@mantine/core' +import { type Series, getSeriesColor } from './chart_container' + +type CurveType = 'linear' | 'monotone' | 'step' | 'stepBefore' | 'stepAfter' + +interface LineChartProps { + data: Record[] + xKey: string + yKey?: string + series?: Series[] + curveType?: CurveType + showGrid?: boolean + showLegend?: boolean + showDots?: boolean + height?: number + xAxisFormatter?: (value: unknown) => string + yAxisFormatter?: (value: unknown) => string + valueFormatter?: (value: number) => string + referenceLines?: Array<{ y: number; label?: string; color?: string }> +} + +function LineChartComponent({ + data, xKey, yKey, series, curveType = 'monotone', showGrid = true, showLegend = false, + showDots = true, height = 300, xAxisFormatter, yAxisFormatter, + valueFormatter = (v) => v.toLocaleString(), referenceLines = [], +}: LineChartProps) { + const chartSeries = series + ? series.map((s, i) => ({ name: s.key, label: s.name, color: getSeriesColor(i, s.color) })) + : yKey ? [{ name: yKey, label: yKey, color: getSeriesColor(0) }] : [] + + const refLines = referenceLines.map((ref) => ({ + y: ref.y, + label: ref.label || '', + color: ref.color || 'gray.6', + })) + + return ( + + + + ) +} + +export const LineChart = LineChartComponent +export type { LineChartProps, CurveType } diff --git a/components/loading_overlay.md b/components/loading_overlay.md new file mode 100644 index 0000000..2c69c4f --- /dev/null +++ b/components/loading_overlay.md @@ -0,0 +1,58 @@ +--- +name: loading_overlay +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnLoadingOverlay(props: FnLoadingOverlayProps): JSX.Element" +description: "Overlay de carga con blur y opacidad configurable. Wrapper sobre Mantine LoadingOverlay." +tags: [mantine, loading, overlay, spinner, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: visible + type: "boolean" + required: true + description: "Controla visibilidad del overlay" + - name: loaderSize + type: "number | string" + required: false + description: "Tamano del loader/spinner" + - name: overlayBlur + type: "number" + required: false + description: "Intensidad del blur del fondo, default 2" + - name: overlayOpacity + type: "number" + required: false + description: "Opacidad del overlay, default 0.5" +output: "Overlay semi-transparente con spinner centrado que cubre el contenedor padre" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/loading_overlay.tsx" +emits: [] +has_state: false +variant: [] +--- + +## Ejemplo + +```tsx +import { FnLoadingOverlay } from '@fn_library' + + + + + +``` + +## Notas + +Wrapper sobre Mantine `LoadingOverlay`. El contenedor padre necesita `position: relative` para que el overlay se posicione correctamente. Usa `loaderProps` y `overlayProps` internamente para mapear las props simplificadas. diff --git a/components/loading_overlay.tsx b/components/loading_overlay.tsx new file mode 100644 index 0000000..34b01f3 --- /dev/null +++ b/components/loading_overlay.tsx @@ -0,0 +1,26 @@ +import { LoadingOverlay } from '@mantine/core' + +interface FnLoadingOverlayProps { + visible: boolean + loaderSize?: number | string + overlayBlur?: number + overlayOpacity?: number +} + +function FnLoadingOverlay({ + visible, + loaderSize, + overlayBlur = 2, + overlayOpacity = 0.5, +}: FnLoadingOverlayProps) { + return ( + + ) +} + +export { FnLoadingOverlay } +export type { FnLoadingOverlayProps } diff --git a/components/mantine_provider.md b/components/mantine_provider.md new file mode 100644 index 0000000..85350d1 --- /dev/null +++ b/components/mantine_provider.md @@ -0,0 +1,59 @@ +--- +name: mantine_provider +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnMantineProvider({ children, theme?, defaultColorScheme? })" +description: "Provider raiz de Mantine para apps del registry. Wrappea MantineProvider con Notifications incluido. Importa los CSS de @mantine/core, charts y notifications." +tags: [mantine, provider, theme, react] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["@mantine/core", "@mantine/notifications", "@mantine/charts"] +framework: react +props: + - name: children + desc: "contenido de la app" + - name: theme + desc: "tema Mantine creado con createTheme() — colores, fuentes, radio, etc." + - name: defaultColorScheme + desc: "esquema de color por defecto: 'dark' | 'light' | 'auto'" +output: "arbol React envuelto en MantineProvider con notificaciones" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/mantine_provider.tsx" +--- + +## Ejemplo + +```tsx +import { createTheme, MantineColorsTuple } from '@mantine/core' +import { FnMantineProvider } from '@fn_library' + +const brand: MantineColorsTuple = [ + '#e5f3ff', '#cde2ff', '#9ac2ff', '#64a0ff', '#3884fe', + '#1d72fe', '#0063ff', '#0058e4', '#004ecd', '#0043b5' +] + +const theme = createTheme({ + colors: { brand }, + primaryColor: 'brand', +}) + +function App() { + return ( + + {/* Tu app aqui */} + + ) +} +``` + +## Notas + +Reemplaza ThemeProvider + applyTheme del sistema anterior. Las apps definen su propio tema con `createTheme()` y lo pasan como prop. Los CSS de Mantine se importan una sola vez aqui. diff --git a/components/mantine_provider.tsx b/components/mantine_provider.tsx new file mode 100644 index 0000000..79ecee0 --- /dev/null +++ b/components/mantine_provider.tsx @@ -0,0 +1,29 @@ +import '@mantine/core/styles.css' +import '@mantine/charts/styles.css' +import '@mantine/notifications/styles.css' + +import * as React from 'react' +import { MantineProvider, type MantineThemeOverride, type MantineColorScheme } from '@mantine/core' +import { Notifications } from '@mantine/notifications' + +interface FnMantineProviderProps { + children: React.ReactNode + theme?: MantineThemeOverride + defaultColorScheme?: MantineColorScheme +} + +function FnMantineProvider({ + children, + theme, + defaultColorScheme = 'dark', +}: FnMantineProviderProps) { + return ( + + + {children} + + ) +} + +export { FnMantineProvider } +export type { FnMantineProviderProps } diff --git a/components/multi_select.md b/components/multi_select.md new file mode 100644 index 0000000..47aff41 --- /dev/null +++ b/components/multi_select.md @@ -0,0 +1,111 @@ +--- +name: multi_select +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "MultiSelect(props: MultiSelectProps): JSX.Element" +description: "Selector múltiple con búsqueda, pills y límite de selecciones. Wrapper sobre Mantine MultiSelect." +tags: [multi-select, form, dropdown, pills, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente MultiSelect que renderiza dropdown con selección múltiple, búsqueda y pills" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/multi_select.tsx" +props: + - name: data + type: "string[] | { value: string; label: string; disabled?: boolean }[]" + required: true + description: "Opciones del selector — strings o objetos {value, label}" + - name: value + type: "string[]" + required: false + description: "Valores seleccionados (controlled)" + - name: onChange + type: "(value: string[]) => void" + required: false + description: "Callback al cambiar la selección" + - name: searchable + type: "boolean" + required: false + description: "Permite buscar entre opciones" + - name: clearable + type: "boolean" + required: false + description: "Muestra botón para limpiar toda la selección" + - name: maxValues + type: "number" + required: false + description: "Número máximo de valores seleccionables" + - name: placeholder + type: "string" + required: false + description: "Texto cuando no hay selección" + - name: label + type: "string" + required: false + description: "Label del campo" + - name: disabled + type: "boolean" + required: false + description: "Deshabilitar el selector" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño del componente" +emits: [onChange] +has_state: true +framework: react +variant: [default] +--- + +## Ejemplo + +```tsx +import { MultiSelect } from '@fn_library' + +// Opciones simples + + +// Con búsqueda y clearable + + +// Máximo de selecciones + +``` + +## Notas + +- Wrapper directo sobre `MultiSelect` de `@mantine/core` v9. Todas las props de Mantine MultiSelect son válidas. +- A diferencia de `Select`, `value` es `string[]` y `onChange` recibe `string[]`. +- Las selecciones se muestran como pills dentro del input, eliminables con clic. +- `maxValues` limita cuántos items pueden seleccionarse — el dropdown bloquea más selecciones al alcanzar el límite. +- Soporta `searchable` para filtrar opciones y `clearable` para un botón de limpiar todo. diff --git a/components/multi_select.tsx b/components/multi_select.tsx new file mode 100644 index 0000000..d174f35 --- /dev/null +++ b/components/multi_select.tsx @@ -0,0 +1,10 @@ +import { MultiSelect as MantineMultiSelect, type MultiSelectProps as MantineMultiSelectProps } from '@mantine/core' + +interface MultiSelectProps extends MantineMultiSelectProps {} + +function MultiSelect(props: MultiSelectProps) { + return +} + +export { MultiSelect } +export type { MultiSelectProps } diff --git a/components/nav_link.md b/components/nav_link.md new file mode 100644 index 0000000..af01e91 --- /dev/null +++ b/components/nav_link.md @@ -0,0 +1,80 @@ +--- +name: nav_link +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnNavLink(props: FnNavLinkProps): JSX.Element" +description: "Link de navegacion con icono, descripcion y anidamiento. Wrapper sobre Mantine NavLink." +tags: [mantine, navigation, link, sidebar, menu, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: label + type: "string" + required: true + description: "Texto principal del link" + - name: description + type: "string" + required: false + description: "Texto secundario debajo del label" + - name: icon + type: "ReactNode" + required: false + description: "Icono a la izquierda del label" + - name: active + type: "boolean" + required: false + description: "Estado activo/seleccionado" + - name: onClick + type: "MouseEventHandler" + required: false + description: "Callback al hacer click" + - name: href + type: "string" + required: false + description: "URL para navegacion como anchor" + - name: children + type: "ReactNode" + required: false + description: "NavLinks hijos para crear arbol anidado" + - name: opened + type: "boolean" + required: false + description: "Controla si los hijos estan expandidos" + - name: defaultOpened + type: "boolean" + required: false + description: "Estado inicial de expansion de hijos" +output: "Link de navegacion con highlight activo, icono y soporte para sub-items colapsables" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/nav_link.tsx" +emits: [] +has_state: false +variant: [] +--- + +## Ejemplo + +```tsx +import { FnNavLink } from '@fn_library' +import { IconHome, IconSettings } from '@tabler/icons-react' + +} active /> +} defaultOpened> + + + +``` + +## Notas + +Wrapper sobre Mantine `NavLink`. Soporta anidamiento -- pasar `FnNavLink` como children crea un arbol colapsable. El `icon` se mapea a `leftSection` internamente. Ideal para uso dentro de `FnAppShell` navbar. diff --git a/components/nav_link.tsx b/components/nav_link.tsx new file mode 100644 index 0000000..ccb411c --- /dev/null +++ b/components/nav_link.tsx @@ -0,0 +1,44 @@ +import * as React from 'react' +import { NavLink } from '@mantine/core' + +interface FnNavLinkProps { + label: string + description?: string + icon?: React.ReactNode + active?: boolean + onClick?: React.MouseEventHandler + href?: string + children?: React.ReactNode + opened?: boolean + defaultOpened?: boolean +} + +function FnNavLink({ + label, + description, + icon, + active, + onClick, + href, + children, + opened, + defaultOpened, +}: FnNavLinkProps) { + return ( + + {children} + + ) +} + +export { FnNavLink } +export type { FnNavLinkProps } diff --git a/components/number_input.md b/components/number_input.md new file mode 100644 index 0000000..2d3ff34 --- /dev/null +++ b/components/number_input.md @@ -0,0 +1,92 @@ +--- +name: number_input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnNumberInput(props: FnNumberInputProps): JSX.Element" +description: "Input numerico con min/max, step, prefijo y sufijo. Wrapper sobre Mantine NumberInput." +tags: [mantine, input, number, form, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: value + type: "number | string" + required: false + description: "Valor actual del input" + - name: onChange + type: "(value: number | string) => void" + required: false + description: "Callback cuando cambia el valor" + - name: min + type: "number" + required: false + description: "Valor minimo permitido" + - name: max + type: "number" + required: false + description: "Valor maximo permitido" + - name: step + type: "number" + required: false + description: "Incremento/decremento por click" + - name: label + type: "string" + required: false + description: "Etiqueta del input" + - name: description + type: "string" + required: false + description: "Texto descriptivo debajo del input" + - name: error + type: "string" + required: false + description: "Mensaje de error" + - name: placeholder + type: "string" + required: false + description: "Placeholder del input" + - name: prefix + type: "string" + required: false + description: "Texto prefijo dentro del input (ej: $)" + - name: suffix + type: "string" + required: false + description: "Texto sufijo dentro del input (ej: kg)" +output: "Input numerico con controles de incremento, validacion y decoradores de texto" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/number_input.tsx" +emits: [] +has_state: false +variant: [] +--- + +## Ejemplo + +```tsx +import { FnNumberInput } from '@fn_library' + + +``` + +## Notas + +Wrapper sobre Mantine `NumberInput`. Soporta prefix/suffix para decorar el valor visualmente. Los controles de incremento/decremento respetan min/max/step. diff --git a/components/number_input.tsx b/components/number_input.tsx new file mode 100644 index 0000000..4158011 --- /dev/null +++ b/components/number_input.tsx @@ -0,0 +1,48 @@ +import { NumberInput } from '@mantine/core' + +interface FnNumberInputProps { + value?: number | string + onChange?: (value: number | string) => void + min?: number + max?: number + step?: number + label?: string + description?: string + error?: string + placeholder?: string + prefix?: string + suffix?: string +} + +function FnNumberInput({ + value, + onChange, + min, + max, + step, + label, + description, + error, + placeholder, + prefix, + suffix, +}: FnNumberInputProps) { + return ( + + ) +} + +export { FnNumberInput } +export type { FnNumberInputProps } diff --git a/components/page_header.md b/components/page_header.md new file mode 100644 index 0000000..0df5c8e --- /dev/null +++ b/components/page_header.md @@ -0,0 +1,63 @@ +--- +name: page_header +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "PageHeader(props: PageHeaderProps): JSX.Element" +description: "Cabecera de página con título, subtítulo, acciones, back button, tabs integrados, badge y modo sticky. Incluye SimplePageHeader." +tags: [header, page, layout, navigation, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react, "@mantine/core", "@tabler/icons-react"] +output: "Componente PageHeader que renderiza cabecera de página con título, acciones, tabs integrados y modo sticky" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/page_header.tsx" +props: + - name: title + type: "string" + required: true + description: "Título principal" + - name: subtitle + type: "string" + required: false + description: "Subtítulo" + - name: actions + type: "ReactNode" + required: false + description: "Botones de acción" + - name: tabs + type: "TabItem[]" + required: false + description: "Tabs de navegación integrados" + - name: sticky + type: "boolean" + required: false + description: "Header fijo al scroll" +emits: [onBack, onTabChange] +has_state: false +framework: react +variant: [full, simple] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/page-header.tsx" +--- + +## Ejemplo + +```tsx +Export} + tabs={[{ label: "Overview", value: "overview" }, { label: "Analytics", value: "analytics" }]} + activeTab="overview" + onTabChange={setTab} +/> +``` diff --git a/components/page_header.tsx b/components/page_header.tsx new file mode 100644 index 0000000..2e74bd4 --- /dev/null +++ b/components/page_header.tsx @@ -0,0 +1,92 @@ +"use client" + +import * as React from "react" +import { Group, Stack, Title, Text, ActionIcon, Tabs, Box } from "@mantine/core" +import { IconChevronLeft } from "@tabler/icons-react" + +interface TabItem { + label: string + value: string + icon?: React.ReactNode + disabled?: boolean +} + +interface PageHeaderProps { + title: string + subtitle?: string + actions?: React.ReactNode + onBack?: () => void + tabs?: TabItem[] + activeTab?: string + onTabChange?: (value: string) => void + badge?: React.ReactNode + sticky?: boolean +} + +function PageHeader({ + title, subtitle, actions, onBack, tabs, activeTab, onTabChange, + badge, sticky = false, +}: PageHeaderProps) { + return ( + + + + + {onBack && ( + + + + )} + + + {title} + {badge} + + {subtitle && {subtitle}} + + + {actions && {actions}} + + {tabs && tabs.length > 0 && ( + v && onTabChange?.(v)} style={{ marginBottom: 'calc(-1 * var(--mantine-spacing-md))' }}> + + {tabs.map((tab) => ( + + {tab.label} + + ))} + + + )} + + + ) +} + +interface SimplePageHeaderProps { + title: string + description?: string + children?: React.ReactNode +} + +function SimplePageHeader({ title, description, children }: SimplePageHeaderProps) { + return ( + + + {title} + {description && {description}} + + {children && {children}} + + ) +} + +export { PageHeader, SimplePageHeader } diff --git a/components/pagination.md b/components/pagination.md new file mode 100644 index 0000000..40110f3 --- /dev/null +++ b/components/pagination.md @@ -0,0 +1,76 @@ +--- +name: pagination +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Pagination(props: PaginationProps): JSX.Element" +description: "Controles de navegacion de paginas autocontenido. Mantine Pagination." +tags: [pagination, navigation, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Pagination autocontenido que renderiza controles de navegacion de paginas" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/pagination.tsx" +props: + - name: total + type: "number" + required: true + description: "Numero total de paginas" + - name: value + type: "number" + required: false + description: "Pagina actual (controlado)" + - name: defaultValue + type: "number" + required: false + description: "Pagina inicial (no controlado)" + - name: onChange + type: "(page: number) => void" + required: false + description: "Callback al cambiar de pagina" + - name: siblings + type: "number" + required: false + description: "Paginas visibles a cada lado de la actual" + - name: boundaries + type: "number" + required: false + description: "Paginas visibles al inicio y final" + - name: withEdges + type: "boolean" + required: false + description: "Mostrar botones first/last page" + - name: className + type: "string" + required: false + description: "Clases CSS adicionales" +emits: [onChange] +has_state: false +framework: react +variant: [] +--- + +## Ejemplo + +```tsx +// Basico + + +// Controlado + + +// Con botones first/last + +``` + +## Notas + +Usa Mantine Pagination autocontenido. Previous/Next, numeros de pagina y elipsis se generan automaticamente. La API anterior con sub-componentes (PaginationContent, PaginationItem, PaginationLink, etc.) fue reemplazada por un unico componente con props declarativas. Export: Pagination y PaginationProps. diff --git a/components/pagination.tsx b/components/pagination.tsx new file mode 100644 index 0000000..9b27d8b --- /dev/null +++ b/components/pagination.tsx @@ -0,0 +1,43 @@ +import { Pagination as MantinePagination } from "@mantine/core" + +interface PaginationProps { + total: number + value?: number + defaultValue?: number + onChange?: (page: number) => void + siblings?: number + boundaries?: number + withEdges?: boolean + className?: string +} + +function Pagination({ + total, + value, + defaultValue, + onChange, + siblings, + boundaries, + withEdges = false, + className, + ...props +}: PaginationProps) { + return ( + + ) +} + +export { Pagination } +export type { PaginationProps } diff --git a/components/password_input.md b/components/password_input.md new file mode 100644 index 0000000..47c7f8f --- /dev/null +++ b/components/password_input.md @@ -0,0 +1,109 @@ +--- +name: password_input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "PasswordInput(props: PasswordInputProps): JSX.Element" +description: "Input de contraseña con toggle de visibilidad y soporte para indicador de fortaleza. Wrapper sobre Mantine PasswordInput." +tags: [password, input, form, security, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/password_input.tsx" +framework: react +has_state: true +props: + - name: visible + type: "boolean" + required: false + description: "Controla externamente si la contraseña es visible" + - name: onVisibilityChange + type: "(visible: boolean) => void" + required: false + description: "Callback cuando el usuario cambia la visibilidad" + - name: visibilityToggleIcon + type: "React.FC<{ reveal: boolean }>" + required: false + description: "Icono personalizado para el botón de toggle" + - name: value + type: "string" + required: false + description: "Valor controlado del input" + - name: onChange + type: "(event: React.ChangeEvent) => void" + required: false + description: "Callback al cambiar el valor" + - name: label + type: "React.ReactNode" + required: false + description: "Etiqueta del campo" + - name: placeholder + type: "string" + required: false + description: "Texto placeholder cuando está vacío" + - name: error + type: "React.ReactNode" + required: false + description: "Mensaje de error a mostrar bajo el input" + - name: disabled + type: "boolean" + required: false + description: "Deshabilita el input" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño del componente" +emits: [onChange, onVisibilityChange] +params: + - name: props + desc: "Props de Mantine PasswordInput: visible, onVisibilityChange, visibilityToggleIcon, value, onChange, label, placeholder, error, disabled, size y cualquier prop HTML nativa de input" +output: "Componente PasswordInput que renderiza input enmascarado con botón de mostrar/ocultar" +--- + +## Ejemplo + +```tsx +import { PasswordInput } from '@fn_library' + +// Uso básico +function LoginForm() { + return ( + + ) +} + +// Con visibilidad controlada +import { useState } from 'react' + +function ControlledPasswordInput() { + const [visible, setVisible] = useState(false) + const [value, setValue] = useState('') + + return ( + setValue(e.currentTarget.value)} + visible={visible} + onVisibilityChange={setVisible} + error={value.length > 0 && value.length < 8 ? 'Mínimo 8 caracteres' : undefined} + /> + ) +} +``` + +## Notas + +Wrapper delgado sobre `PasswordInput` de Mantine v9. El toggle de visibilidad es nativo de Mantine y se controla opcionalmente con `visible` + `onVisibilityChange`. Soporta todas las props de Mantine incluyendo `strengthMeter` para indicadores de fortaleza. Los iconos del toggle deben venir de `@tabler/icons-react`. diff --git a/components/password_input.tsx b/components/password_input.tsx new file mode 100644 index 0000000..288121e --- /dev/null +++ b/components/password_input.tsx @@ -0,0 +1,10 @@ +import { PasswordInput as MantinePasswordInput, type PasswordInputProps as MantinePasswordInputProps } from '@mantine/core' + +interface PasswordInputProps extends MantinePasswordInputProps {} + +function PasswordInput(props: PasswordInputProps) { + return +} + +export { PasswordInput } +export type { PasswordInputProps } diff --git a/components/pie_chart.md b/components/pie_chart.md new file mode 100644 index 0000000..c63d2c0 --- /dev/null +++ b/components/pie_chart.md @@ -0,0 +1,88 @@ +--- +name: pie_chart +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "PieChart(props: PieChartProps): JSX.Element" +description: "Gráfico de torta/dona @mantine/charts con colores automáticos, labels y tooltip. Usa DonutChart para dona, PieChart para torta." +tags: [chart, pie, donut, visualization, mantine, component, ui, dashboard] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/charts", "@mantine/core"] +output: "Componente PieChart que renderiza gráfico de torta o dona con labels y tooltip" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/pie_chart.tsx" +props: + - name: data + type: "Record[]" + required: true + description: "Array de datos. Los valores de valueKey se convierten a number automáticamente." + - name: nameKey + type: "string" + required: true + description: "Key del campo que contiene el nombre/etiqueta de cada segmento" + - name: valueKey + type: "string" + required: true + description: "Key del campo numérico que determina el tamaño de cada segmento" + - name: colors + type: "string[]" + required: false + description: "Paleta de colores hex. Default: 8 colores accesibles. Se repite cíclicamente." + - name: donut + type: "boolean" + required: false + description: "Modo dona. innerRadius pasa a 50 por defecto cuando donut=true." + - name: showLegend + type: "boolean" + required: false + description: "Mostrar leyenda. Default true." + - name: showLabels + type: "boolean" + required: false + description: "Mostrar labels nombre+% en cada segmento. Default true." + - name: height + type: "number | string" + required: false + description: "Altura del contenedor. Default 300." + - name: valueFormatter + type: "(value: number) => string" + required: false + description: "Formateador de valores para el tooltip. Default toLocaleString." +emits: [] +has_state: false +framework: react +variant: [pie, donut] +--- + +## Ejemplo + +```tsx +// Pie simple + + +// Dona sin labels + `${v} fns`} +/> +``` + +## Notas + +Los valores de `valueKey` se convierten a `Number()` antes de pasarlos al chart. Cuando `donut=true` se usa `DonutChart` de Mantine, de lo contrario `PieChart`. diff --git a/components/pie_chart.tsx b/components/pie_chart.tsx new file mode 100644 index 0000000..b3dd3fb --- /dev/null +++ b/components/pie_chart.tsx @@ -0,0 +1,58 @@ +import { PieChart as MantinePieChart, DonutChart } from '@mantine/charts' +import { Paper } from '@mantine/core' + +const DEFAULT_COLORS = [ + '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', + '#ec4899', '#06b6d4', '#f97316', +] + +interface PieChartProps { + data: Record[] + nameKey: string + valueKey: string + colors?: string[] + donut?: boolean + showLegend?: boolean + showLabels?: boolean + height?: number + valueFormatter?: (value: number) => string +} + +function PieChartComponent({ + data, + nameKey, + valueKey, + colors = DEFAULT_COLORS, + donut = false, + showLegend: _showLegend = true, + showLabels = true, + height = 300, + valueFormatter = (v) => v.toLocaleString(), +}: PieChartProps) { + const chartData = data.map((row, i) => ({ + name: String(row[nameKey] ?? ''), + value: Number(row[valueKey]) || 0, + color: colors[i % colors.length] as string, + })) + + const Chart = donut ? DonutChart : MantinePieChart + + return ( + + + {/* Mantine PieChart/DonutChart does not have a built-in legend prop; + legend is handled via withLabels. showLegend kept for API compat. */} + + ) +} + +export const PieChart = PieChartComponent +export type { PieChartProps } diff --git a/components/pin_input.md b/components/pin_input.md new file mode 100644 index 0000000..e568876 --- /dev/null +++ b/components/pin_input.md @@ -0,0 +1,105 @@ +--- +name: pin_input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "PinInput(props: PinInputProps): JSX.Element" +description: "Input de código PIN/OTP con campos individuales y autocompletado. Wrapper sobre Mantine PinInput." +tags: [pin, otp, code, input, form, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente PinInput que renderiza campos individuales por dígito con soporte para autocompletado OTP, máscaras y teclado numérico o alfanumérico" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/pin_input.tsx" +props: + - name: length + type: "number" + required: false + description: "Número de campos individuales del PIN (por defecto 4)" + - name: type + type: "'number' | 'alphanumeric'" + required: false + description: "Tipo de caracteres aceptados — solo números o también letras" + - name: mask + type: "boolean" + required: false + description: "Oculta los caracteres ingresados como contraseña" + - name: oneTimeCode + type: "boolean" + required: false + description: "Activa el atributo autocomplete=one-time-code para OTP en móvil" + - name: value + type: "string" + required: false + description: "Valor actual del PIN (controlled)" + - name: onChange + type: "(value: string) => void" + required: false + description: "Callback al cambiar cualquier campo" + - name: onComplete + type: "(value: string) => void" + required: false + description: "Callback cuando todos los campos están completos" + - name: placeholder + type: "string" + required: false + description: "Carácter placeholder en cada campo vacío" + - name: disabled + type: "boolean" + required: false + description: "Deshabilita todos los campos" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño visual de los campos" +emits: [onChange, onComplete] +has_state: true +framework: react +variant: [default] +--- + +## Ejemplo + +```tsx +import { PinInput } from '@fn_library' + +// PIN numérico de 4 dígitos + console.log('PIN completo:', value)} +/> + +// OTP de 6 dígitos con autocompletado móvil + + +// PIN enmascarado alfanumérico + +``` + +## Notas + +- Wrapper directo sobre `PinInput` de `@mantine/core` v9. Todas las props de Mantine PinInput son válidas. +- `oneTimeCode` genera `autocomplete="one-time-code"` para que el browser/SMS sugiera el código automáticamente. +- Cada campo avanza al siguiente automáticamente al completarse. +- Soporta navegación con teclado (flechas, backspace) entre campos. diff --git a/components/pin_input.tsx b/components/pin_input.tsx new file mode 100644 index 0000000..d6167d5 --- /dev/null +++ b/components/pin_input.tsx @@ -0,0 +1,10 @@ +import { PinInput as MantinePinInput, type PinInputProps as MantinePinInputProps } from '@mantine/core' + +interface PinInputProps extends MantinePinInputProps {} + +function PinInput(props: PinInputProps) { + return +} + +export { PinInput } +export type { PinInputProps } diff --git a/components/popover.md b/components/popover.md new file mode 100644 index 0000000..46f7e11 --- /dev/null +++ b/components/popover.md @@ -0,0 +1,66 @@ +--- +name: popover +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Popover(props: PopoverProps): JSX.Element" +description: "Contenido flotante posicionado accesible con animaciones. Mantine Popover." +tags: [popover, component, ui, interactive, overlay, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", react] +output: "Componente Popover que renderiza contenido flotante accesible posicionado automáticamente via Mantine Popover" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/popover.tsx" +props: + - name: open + type: "boolean" + required: false + description: "Estado controlado de apertura" + - name: defaultOpen + type: "boolean" + required: false + description: "Estado inicial de apertura (no controlado)" + - name: onOpenChange + type: "(open: boolean) => void" + required: false + description: "Callback cuando cambia el estado de apertura" + - name: sideOffset + type: "number" + required: false + description: "Distancia en px entre trigger y popover (default: 4)" +emits: [onOpenChange] +has_state: false +framework: react +variant: [] +--- + +## Ejemplo + +```tsx + + + + + + + Configuracion + Ajusta tus preferencias. + +
    + {/* contenido */} +
    +
    +
    +``` + +## Notas + +Compuesto de: Popover (root), PopoverTrigger, PopoverContent, PopoverClose, PopoverHeader, PopoverTitle, PopoverDescription. El posicionamiento automatico lo maneja Mantine Popover. PopoverPortal es un no-op mantenido por compatibilidad. diff --git a/components/popover.tsx b/components/popover.tsx new file mode 100644 index 0000000..e410f26 --- /dev/null +++ b/components/popover.tsx @@ -0,0 +1,92 @@ +import * as React from 'react' +import { Popover as MantinePopover, Box, Text } from '@mantine/core' + +interface PopoverProps { + open?: boolean + defaultOpen?: boolean + onOpenChange?: (open: boolean) => void + children: React.ReactNode +} + +function Popover({ open, defaultOpen, onOpenChange, children }: PopoverProps) { + return ( + + {children} + + ) +} + +function PopoverTrigger({ children }: React.ComponentProps<'div'>) { + return ( + + {React.isValidElement(children) ? ( + children + ) : ( + + )} + + ) +} + +function PopoverPortal({ children }: { children: React.ReactNode }) { + return <>{children} +} + +function PopoverContent({ className, children, sideOffset, ...props }: React.ComponentProps<'div'> & { sideOffset?: number }) { + return ( + + {children} + + ) +} + +function PopoverClose({ children, ...props }: React.ComponentProps<'button'>) { + return ( + + ) +} + +function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) { + return +} + +function PopoverTitle({ className, ...props }: React.ComponentProps<'h4'>) { + return ( + + ) +} + +function PopoverDescription({ className, ...props }: React.ComponentProps<'p'>) { + return ( + + ) +} + +export { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverPortal, PopoverTitle, PopoverTrigger } diff --git a/components/progress_bar.md b/components/progress_bar.md new file mode 100644 index 0000000..52e853e --- /dev/null +++ b/components/progress_bar.md @@ -0,0 +1,79 @@ +--- +name: progress_bar +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "ProgressBar(props: ProgressBarProps): JSX.Element" +description: "Barra de progreso con variantes de color y tamaño, buffer, animación, modo indeterminado y display de valor. Mantine Progress." +tags: [progress, loading, component, ui, feedback, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente ProgressBar que renderiza barra de progreso con animaciones, buffer y modo indeterminado" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/progress_bar.tsx" +props: + - name: value + type: "number" + required: true + description: "Valor actual de progreso" + - name: max + type: "number" + required: false + description: "Valor máximo (default 100)" + - name: buffer + type: "number" + required: false + description: "Valor de buffer secundario (opcional)" + - name: showValue + type: "boolean" + required: false + description: "Mostrar porcentaje como texto superpuesto" + - name: animated + type: "boolean" + required: false + description: "Activar animación de rayas (stripes)" + - name: indeterminate + type: "boolean" + required: false + description: "Modo indeterminado sin valor conocido" + - name: size + type: "'sm' | 'md' | 'lg'" + required: false + description: "Altura de la barra (default md)" + - name: color + type: "'primary' | 'success' | 'warning' | 'destructive'" + required: false + description: "Color semántico (default primary)" + - name: label + type: "string" + required: false + description: "aria-label para accesibilidad" +emits: [] +has_state: false +framework: react +variant: [primary, success, warning, destructive] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/progress/progress-bar.tsx" +--- + +## Ejemplo + +```tsx + + + + +``` + +## Notas + +El porcentaje se clampea a [0, 100] internamente. El buffer se renderiza como capa semitransparente debajo del fill usando Progress.Root apilados. Los colores se mapean de semanticos (primary/success/warning/destructive) a colores Mantine. La animacion striped usa el prop animated nativo de Mantine. diff --git a/components/progress_bar.tsx b/components/progress_bar.tsx new file mode 100644 index 0000000..f2d2d75 --- /dev/null +++ b/components/progress_bar.tsx @@ -0,0 +1,87 @@ +import { Progress, Box } from "@mantine/core" + +export interface ProgressBarProps { + value: number + max?: number + buffer?: number + showValue?: boolean + animated?: boolean + indeterminate?: boolean + label?: string + size?: "sm" | "md" | "lg" + color?: "primary" | "success" | "warning" | "destructive" + className?: string +} + +const colorMap: Record, string> = { + primary: "blue", + success: "green", + warning: "yellow", + destructive: "red", +} + +const sizeMap: Record, number> = { + sm: 4, + md: 8, + lg: 12, +} + +export function ProgressBar({ + value, + max = 100, + buffer, + showValue = false, + animated = false, + indeterminate = false, + size = "md", + color = "primary", + label, + className, +}: ProgressBarProps) { + const percentage = Math.min(100, Math.max(0, (value / max) * 100)) + const bufferPercentage = buffer ? Math.min(100, Math.max(0, (buffer / max) * 100)) : undefined + const mantineColor = colorMap[color] + const mantineSize = sizeMap[size] + + if (bufferPercentage !== undefined) { + return ( + + + + + + + + {showValue && !indeterminate && ( + {Math.round(percentage)}% + )} + + + + + ) + } + + return ( + + + {showValue && !indeterminate && ( + {Math.round(percentage)}% + )} + + + ) +} diff --git a/components/radio_group.md b/components/radio_group.md new file mode 100644 index 0000000..7dca768 --- /dev/null +++ b/components/radio_group.md @@ -0,0 +1,61 @@ +--- +name: radio_group +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "RadioGroup(props: RadioGroupProps): JSX.Element" +description: "Grupo de opciones exclusivas accesible. Mantine Radio.Group + Radio." +tags: [radio, radio-group, component, ui, interactive, form, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente RadioGroup que renderiza grupo de opciones exclusivas accesible" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/radio_group.tsx" +props: + - name: value + type: "string" + required: false + description: "Valor seleccionado (controlado)" + - name: defaultValue + type: "string" + required: false + description: "Valor inicial (no controlado)" + - name: onValueChange + type: "(value: string) => void" + required: false + description: "Callback al cambiar seleccion" + - name: disabled + type: "boolean" + required: false + description: "Deshabilita todo el grupo" + - name: orientation + type: "'horizontal' | 'vertical'" + required: false + description: "Orientacion del grupo" +emits: [onValueChange] +has_state: false +framework: react +variant: [] +--- + +## Ejemplo + +```tsx + + + + + +``` + +## Notas + +RadioGroup es el contenedor (Mantine Radio.Group). RadioGroupItem es cada opcion individual (Mantine Radio). Soporta orientacion horizontal y vertical. diff --git a/components/radio_group.tsx b/components/radio_group.tsx new file mode 100644 index 0000000..64911f4 --- /dev/null +++ b/components/radio_group.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Radio } from "@mantine/core" + +interface RadioGroupProps { + value?: string + defaultValue?: string + onValueChange?: (value: string) => void + disabled?: boolean + orientation?: "horizontal" | "vertical" + className?: string + children?: React.ReactNode +} + +function RadioGroup({ className, value, defaultValue, onValueChange, orientation, children, ...props }: RadioGroupProps) { + return ( + onValueChange?.(val)} + className={className} + {...props} + > +
    + {children} +
    +
    + ) +} + +interface RadioGroupItemProps { + value: string + label?: string + disabled?: boolean + className?: string + labelClassName?: string + id?: string +} + +function RadioGroupItem({ className, label, id, disabled, value, ...props }: RadioGroupItemProps) { + return ( + + ) +} + +export { RadioGroup, RadioGroupItem } +export type { RadioGroupItemProps } diff --git a/components/rating.md b/components/rating.md new file mode 100644 index 0000000..6610db2 --- /dev/null +++ b/components/rating.md @@ -0,0 +1,94 @@ +--- +name: rating +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Rating(props: RatingProps): JSX.Element" +description: "Selector de calificación con estrellas, fracciones y símbolos custom. Wrapper sobre Mantine Rating." +tags: [rating, stars, feedback, input, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Rating que renderiza un selector de estrellas interactivo con soporte para fracciones, modo solo lectura y símbolos personalizados" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/rating.tsx" +props: + - name: count + type: "number" + required: false + description: "Número de estrellas/símbolos a mostrar (por defecto 5)" + - name: fractions + type: "number" + required: false + description: "Divisiones por estrella — 2 para mitades, 4 para cuartos" + - name: value + type: "number" + required: false + description: "Calificación actual (controlled)" + - name: onChange + type: "(value: number) => void" + required: false + description: "Callback al cambiar la calificación" + - name: readOnly + type: "boolean" + required: false + description: "Modo solo lectura — muestra la calificación sin permitir cambios" + - name: highlightSelectedOnly + type: "boolean" + required: false + description: "Solo resalta el símbolo seleccionado, no los anteriores" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño de los símbolos" + - name: color + type: "string" + required: false + description: "Color de los símbolos activos (color de Mantine o CSS)" +emits: [onChange] +has_state: true +framework: react +variant: [default] +--- + +## Ejemplo + +```tsx +import { Rating } from '@fn_library' + +// Rating básico de 5 estrellas + + +// Con medias estrellas + + +// Solo lectura para mostrar calificación + + +// Con más estrellas y color custom + +``` + +## Notas + +- Wrapper directo sobre `Rating` de `@mantine/core` v9. Todas las props de Mantine Rating son válidas. +- `fractions` permite selección granular: 2 = mitades (0.5, 1, 1.5...), 4 = cuartos (0.25, 0.5...). +- En modo `readOnly` no dispara `onChange` y el cursor es el por defecto. +- Accesible via teclado con soporte para lectores de pantalla. diff --git a/components/rating.tsx b/components/rating.tsx new file mode 100644 index 0000000..4209277 --- /dev/null +++ b/components/rating.tsx @@ -0,0 +1,10 @@ +import { Rating as MantineRating, type RatingProps as MantineRatingProps } from '@mantine/core' + +interface RatingProps extends MantineRatingProps {} + +function Rating(props: RatingProps) { + return +} + +export { Rating } +export type { RatingProps } diff --git a/components/ring_progress.md b/components/ring_progress.md new file mode 100644 index 0000000..4f66ca2 --- /dev/null +++ b/components/ring_progress.md @@ -0,0 +1,67 @@ +--- +name: ring_progress +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnRingProgress(props: FnRingProgressProps): JSX.Element" +description: "Anillo de progreso con secciones coloreadas y label central. Wrapper sobre Mantine RingProgress." +tags: [mantine, progress, ring, chart, metrics, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: sections + type: "RingProgressSection[]" + required: true + description: "Secciones del anillo con valor porcentual y color" + - name: size + type: "number" + required: false + description: "Tamano del anillo en px, default 120" + - name: thickness + type: "number" + required: false + description: "Grosor del trazo en px, default 12" + - name: label + type: "ReactNode" + required: false + description: "Contenido central del anillo (texto, icono, etc)" + - name: rootColor + type: "string" + required: false + description: "Color del fondo del anillo (parte sin llenar)" +output: "Anillo de progreso SVG con secciones coloreadas y slot central para label" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/ring_progress.tsx" +emits: [] +has_state: false +variant: [] +--- + +## Ejemplo + +```tsx +import { FnRingProgress } from '@fn_library' +import { Text } from '@mantine/core' + +80%
    } +/> +``` + +## Notas + +Wrapper sobre Mantine `RingProgress`. Las secciones son aditivas -- la suma de `value` no deberia exceder 100. El `label` se renderiza centrado dentro del anillo. diff --git a/components/ring_progress.tsx b/components/ring_progress.tsx new file mode 100644 index 0000000..487377b --- /dev/null +++ b/components/ring_progress.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' +import { RingProgress } from '@mantine/core' +import type { RingProgressSection } from '@mantine/core' + +interface FnRingProgressProps { + sections: RingProgressSection[] + size?: number + thickness?: number + label?: React.ReactNode + rootColor?: string +} + +function FnRingProgress({ + sections, + size = 120, + thickness = 12, + label, + rootColor, +}: FnRingProgressProps) { + return ( + + ) +} + +export { FnRingProgress } +export type { FnRingProgressProps } diff --git a/components/search_bar.md b/components/search_bar.md new file mode 100644 index 0000000..5ee9598 --- /dev/null +++ b/components/search_bar.md @@ -0,0 +1,72 @@ +--- +name: search_bar +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "SearchBar(props: SearchBarProps): JSX.Element" +description: "Search input with debounce, search icon, and clear button" +tags: [component, ui, search, input, debounce] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", "@tabler/icons-react"] +params: + - name: onSearch + desc: "Callback que se ejecuta con la query debounceada" + - name: placeholder + desc: "Placeholder del input (por defecto 'Search...')" + - name: debounceMs + desc: "Delay en milisegundos para el debounce (por defecto 300)" +output: "Componente SearchBar que renderiza input de búsqueda con icono y botón de limpiar" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/search_bar.tsx" +props: + - name: onSearch + type: "(query: string) => void" + required: true + description: "Called with the debounced search query" + - name: placeholder + type: "string" + required: false + description: "Placeholder text (default: Search...)" + - name: debounceMs + type: "number" + required: false + description: "Debounce delay in ms (default: 300)" + - name: className + type: "string" + required: false + description: "Additional CSS classes" +emits: [] +has_state: true +framework: react +variant: [] +--- + +## Ejemplo + +```tsx +import { SearchBar } from '@fn_library' + +function MyPage() { + return ( + console.log('search:', query)} + placeholder="Search entities..." + debounceMs={300} + /> + ) +} +``` + +## Notas + +- Debounce usa ref para evitar re-renders innecesarios del callback +- El icono de clear solo aparece cuando hay texto +- Usa CSS variables del tema para colores (border, input, foreground, muted-foreground) diff --git a/components/search_bar.tsx b/components/search_bar.tsx new file mode 100644 index 0000000..8137bb0 --- /dev/null +++ b/components/search_bar.tsx @@ -0,0 +1,49 @@ +import * as React from 'react' +import { TextInput, CloseButton } from '@mantine/core' +import { IconSearch } from '@tabler/icons-react' + +interface SearchBarProps { + onSearch: (query: string) => void + placeholder?: string + debounceMs?: number + className?: string +} + +function SearchBar({ + onSearch, + placeholder = 'Search...', + debounceMs = 300, + className, +}: SearchBarProps) { + const [query, setQuery] = React.useState('') + const timerRef = React.useRef | null>(null) + const onSearchRef = React.useRef(onSearch) + onSearchRef.current = onSearch + + React.useEffect(() => { + if (timerRef.current) clearTimeout(timerRef.current) + timerRef.current = setTimeout(() => { + onSearchRef.current(query) + }, debounceMs) + return () => { + if (timerRef.current) clearTimeout(timerRef.current) + } + }, [query, debounceMs]) + + return ( + setQuery(e.currentTarget.value)} + placeholder={placeholder} + leftSection={} + rightSection={query ? ( + setQuery('')} aria-label="Clear search" /> + ) : undefined} + className={className} + size="sm" + /> + ) +} + +export { SearchBar } +export type { SearchBarProps } diff --git a/components/segmented_control.md b/components/segmented_control.md new file mode 100644 index 0000000..1b3e55a --- /dev/null +++ b/components/segmented_control.md @@ -0,0 +1,71 @@ +--- +name: segmented_control +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnSegmentedControl(props: FnSegmentedControlProps): JSX.Element" +description: "Control segmentado para seleccion unica entre opciones. Wrapper sobre Mantine SegmentedControl." +tags: [mantine, segmented, toggle, tabs, selection, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: data + type: "{ value: string; label: string }[]" + required: true + description: "Opciones del control segmentado" + - name: value + type: "string" + required: false + description: "Valor seleccionado actualmente" + - name: onChange + type: "(value: string) => void" + required: false + description: "Callback cuando cambia la seleccion" + - name: fullWidth + type: "boolean" + required: false + description: "Ocupa todo el ancho disponible, default false" + - name: size + type: "MantineSize" + required: false + description: "Tamano del control, default sm" + - name: color + type: "MantineColor" + required: false + description: "Color del segmento activo" +output: "Control segmentado con animacion de slide entre opciones" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/segmented_control.tsx" +emits: [] +has_state: false +variant: [] +--- + +## Ejemplo + +```tsx +import { FnSegmentedControl } from '@fn_library' + + +``` + +## Notas + +Wrapper sobre Mantine `SegmentedControl`. Alternativa a tabs para seleccion compacta entre pocas opciones. La animacion de slide es nativa de Mantine. diff --git a/components/segmented_control.tsx b/components/segmented_control.tsx new file mode 100644 index 0000000..d35f78c --- /dev/null +++ b/components/segmented_control.tsx @@ -0,0 +1,39 @@ +import { SegmentedControl } from '@mantine/core' +import type { MantineSize, MantineColor } from '@mantine/core' + +interface SegmentedItem { + value: string + label: string +} + +interface FnSegmentedControlProps { + data: SegmentedItem[] + value?: string + onChange?: (value: string) => void + fullWidth?: boolean + size?: MantineSize + color?: MantineColor +} + +function FnSegmentedControl({ + data, + value, + onChange, + fullWidth = false, + size = 'sm', + color, +}: FnSegmentedControlProps) { + return ( + + ) +} + +export { FnSegmentedControl } +export type { FnSegmentedControlProps, SegmentedItem } diff --git a/components/select.md b/components/select.md new file mode 100644 index 0000000..8043da9 --- /dev/null +++ b/components/select.md @@ -0,0 +1,106 @@ +--- +name: select +kind: component +lang: ts +domain: ui +version: "2.0.0" +purity: impure +signature: "Select(props: SelectProps): JSX.Element" +description: "Select dropdown con búsqueda, grupos y accesibilidad. Wrapper sobre Mantine Select con API declarativa via prop data." +tags: [select, form, dropdown, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +output: "Componente Select que renderiza dropdown searchable con soporte para opciones planas, agrupadas y custom render" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/select.tsx" +props: + - name: data + type: "string[] | { value: string; label: string; disabled?: boolean }[] | { group: string; items: ... }[]" + required: true + description: "Opciones del select — strings, objetos {value,label}, o grupos" + - name: value + type: "string | null" + required: false + description: "Valor seleccionado (controlled)" + - name: onChange + type: "(value: string | null) => void" + required: false + description: "Callback al cambiar selección" + - name: defaultValue + type: "string | null" + required: false + description: "Valor inicial (uncontrolled)" + - name: placeholder + type: "string" + required: false + description: "Texto cuando no hay selección" + - name: label + type: "string" + required: false + description: "Label del campo" + - name: searchable + type: "boolean" + required: false + description: "Permite buscar entre opciones" + - name: clearable + type: "boolean" + required: false + description: "Permite limpiar la selección" + - name: disabled + type: "boolean" + required: false + description: "Deshabilitar el select" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño del componente" +emits: [onChange] +has_state: true +framework: react +variant: [default] +--- + +## Ejemplo + +```tsx +import { Select } from '@fn_library' + +// Opciones simples (strings) + + +// Con grupos +