merge: quick/frontmatter-fixes-new-components-osint — frontmatter fixes, UI components, OSINT types, indexer warnings

This commit is contained in:
2026-04-03 03:24:20 +02:00
126 changed files with 4167 additions and 99 deletions
+569
View File
@@ -0,0 +1,569 @@
# /frontend — Skill para proyectos frontend
Eres un arquitecto frontend experto. Esta skill se activa cuando el usuario pide crear un proyecto frontend, una app con UI, un componente nuevo, o una feature frontend. Tu trabajo es garantizar que TODO el frontend se construya usando el sistema de funciones reutilizables del registry y las mejores practicas actuales.
---
## PASO 1: Consultar el registry (OBLIGATORIO)
Antes de escribir una sola linea de codigo, consulta registry.db para saber que componentes, funciones y tipos frontend ya existen:
```bash
# Componentes y funciones frontend disponibles
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE lang IN ('ts','typescript') ORDER BY domain, name;"
# Tipos frontend disponibles
sqlite3 registry.db "SELECT id, algebraic, description FROM types WHERE lang IN ('ts','typescript') ORDER BY domain, name;"
# Busqueda FTS5 si buscas algo especifico
sqlite3 registry.db "SELECT id, kind, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'name:chart* OR description:chart*') ORDER BY name;"
```
Tambien lista los archivos reales en disco ya que no todos estan indexados aun:
```bash
ls frontend/functions/ui/ # Componentes React
ls frontend/functions/core/ # Utilidades TS puras
ls frontend/types/ # Tipos
```
**REGLA:** Si un componente ya existe en `frontend/functions/ui/` (alias `@fn_library`), USALO. Nunca recrear lo que ya existe.
---
## PASO 2: Determinar el tipo de trabajo
### A) App nueva en `apps/`
Ir a → Seccion SCAFFOLD APP
### B) Componente nuevo para el registry
Ir a → Seccion CREAR COMPONENTE
### C) Feature en app existente
Ir a → Seccion CREAR FEATURE
---
## SCAFFOLD APP
Crear la estructura completa de una app frontend nueva en `apps/{nombre}/frontend/`.
### Estructura obligatoria
```
apps/{nombre}/
frontend/
package.json
vite.config.ts
tsconfig.json
index.html
src/
main.tsx # Entry point
App.tsx # Root con ThemeProvider + Router
app.css # Tokens CSS — NUNCA hardcodear colores
features/ # Feature-based co-location
{feature}/
components/ # Componentes del feature
hooks/ # Hooks del feature
types.ts # Tipos del feature
index.ts # Barrel export publico
components/ # Componentes compartidos de esta app (no reutilizables)
hooks/ # Hooks compartidos
lib/ # Utilidades, API client
types/ # Tipos globales de la app
```
### package.json base
```json
{
"name": "{nombre}",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "tsc -b && vite build",
"preview": "vite preview --host"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.577.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^2.15.0",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"tailwindcss": "^4.2.2",
"typescript": "~5.9.3",
"vite": "^8.0.0"
}
}
```
Agregar dependencias extras segun necesidad:
- **Tablas**: `@tanstack/react-table`
- **Charts**: `recharts`
- **Iconos extra**: `@phosphor-icons/react`
- **Forms**: `react-hook-form`, `@hookform/resolvers`, `zod`
- **Router**: `react-router` o `@tanstack/react-router`
- **State**: `zustand` (client state), `@tanstack/react-query` (server state)
- **Wails**: los hooks de Wails ya estan en `@fn_library` (useWailsQuery, useWailsMutation, useWailsStream, useWailsEvent, WailsProvider)
### vite.config.ts base
```ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { resolve } from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@fn_library': resolve(__dirname, '../../../frontend/functions/ui'),
},
dedupe: ['react', 'react-dom'],
},
build: {
target: 'es2022',
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom'],
},
},
},
},
})
```
### app.css base
```css
@import "tailwindcss";
@theme inline {
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
}
/* Dark theme (default) */
:root,
[data-theme="dark"] {
--background: oklch(8% 0.015 260);
--foreground: oklch(95% 0.01 260);
--muted: oklch(18% 0.02 260);
--muted-foreground: oklch(60% 0.02 260);
--border: oklch(15% 0.01 260);
--primary: oklch(65% 0.22 260);
--primary-foreground: oklch(98% 0.01 260);
--secondary: oklch(20% 0.02 260);
--secondary-foreground: oklch(95% 0.01 260);
--accent: oklch(18% 0.03 260);
--accent-foreground: oklch(95% 0.01 260);
--destructive: oklch(55% 0.22 25);
--destructive-foreground: oklch(98% 0.01 260);
--card: oklch(11% 0.015 260);
--card-foreground: oklch(95% 0.01 260);
--popover: oklch(12% 0.015 260);
--popover-foreground: oklch(95% 0.01 260);
--ring: oklch(65% 0.22 260);
--input: oklch(22% 0.02 260);
--radius: 0.5rem;
--chart-1: oklch(62% 0.19 260);
--chart-2: oklch(65% 0.2 155);
--chart-3: oklch(75% 0.18 85);
--chart-4: oklch(60% 0.22 25);
--chart-5: oklch(60% 0.2 300);
}
/* Light theme */
[data-theme="light"] {
--background: oklch(99% 0.005 260);
--foreground: oklch(15% 0.01 260);
--muted: oklch(95% 0.01 260);
--muted-foreground: oklch(45% 0.02 260);
--border: oklch(90% 0.01 260);
--primary: oklch(50% 0.22 260);
--primary-foreground: oklch(98% 0.01 260);
--secondary: oklch(95% 0.01 260);
--secondary-foreground: oklch(20% 0.01 260);
--accent: oklch(95% 0.02 260);
--accent-foreground: oklch(20% 0.01 260);
--destructive: oklch(55% 0.22 25);
--destructive-foreground: oklch(98% 0.01 260);
--card: oklch(100% 0 0);
--card-foreground: oklch(15% 0.01 260);
--popover: oklch(100% 0 0);
--popover-foreground: oklch(15% 0.01 260);
--ring: oklch(50% 0.22 260);
--input: oklch(90% 0.01 260);
--radius: 0.5rem;
--chart-1: oklch(55% 0.22 260);
--chart-2: oklch(55% 0.2 155);
--chart-3: oklch(65% 0.18 85);
--chart-4: oklch(55% 0.22 25);
--chart-5: oklch(55% 0.2 300);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
### App.tsx base
```tsx
import { ThemeProvider } from '@fn_library'
export default function App() {
return (
<ThemeProvider defaultTheme="dark">
{/* Router y contenido aqui */}
</ThemeProvider>
)
}
```
### Despues del scaffold
```bash
cd apps/{nombre}/frontend && pnpm install
```
---
## CREAR COMPONENTE
Para componentes nuevos que van al registry en `frontend/functions/`.
### Reglas de implementacion
1. **Headless first**: usar `@base-ui/react` como primitivo si el componente es interactivo (dialog, select, tooltip, etc.)
2. **CVA para variantes**: SIEMPRE usar `class-variance-authority` para definir variantes
3. **cn() para clases**: SIEMPRE usar `cn()` de `frontend/functions/core/cn.ts` para componer classNames
4. **CSS variables**: NUNCA hex/rgb/oklch inline en el componente — solo clases Tailwind que mapean a CSS variables (`bg-primary`, `text-muted-foreground`, `border-border`)
5. **Props tipadas**: usar `React.ComponentPropsWithoutRef<"element">` para HTML props spreading
6. **Accesibilidad**:
- Elementos semanticos: `<button>` para acciones, `<a>` para navegacion, `<dialog>` para modales
- NUNCA `<div onClick>` para elementos interactivos
- `aria-label` o `aria-labelledby` en todo componente interactivo
- `aria-invalid` + `aria-describedby` en inputs con error
- `role="status"` para loading states
- Focus management en modales/popovers
7. **Discriminated unions** cuando las props cambian segun variante:
```tsx
type Props = { size?: 'sm' | 'md' | 'lg'; children: React.ReactNode } & (
| { variant: 'link'; href: string; onClick?: never }
| { variant: 'button'; onClick: () => void; href?: never }
)
```
### Patron de archivo .tsx
```tsx
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../core/cn'
const componentVariants = cva(
'base-classes-here', // clases base
{
variants: {
variant: {
default: 'classes...',
secondary: 'classes...',
},
size: {
sm: 'classes...',
md: 'classes...',
lg: 'classes...',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
}
)
interface ComponentProps
extends React.ComponentPropsWithoutRef<'div'>,
VariantProps<typeof componentVariants> {
// props adicionales con JSDoc
/** Descripcion de la prop */
customProp?: string
}
const Component = React.forwardRef<HTMLDivElement, ComponentProps>(
({ className, variant, size, customProp, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(componentVariants({ variant, size }), className)}
{...props}
/>
)
}
)
Component.displayName = 'Component'
export { Component, componentVariants }
export type { ComponentProps }
```
### Patron de archivo .md
**IMPORTANTE:** El campo `lang` debe ser `ts` (no `typescript`). El indexer solo reconoce `ts`. Los IDs siguen el formato `{name}_ts_{domain}`.
```yaml
---
name: component_name
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "ComponentName(props: ComponentProps): JSX.Element"
description: "Descripcion concisa de que hace el componente"
tags: [component, ui, ...]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", "class-variance-authority"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/component_name.tsx"
props:
- name: variant
type: "'default' | 'secondary'"
required: false
description: "Estilo visual"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: [default, secondary]
---
## Ejemplo
...codigo de ejemplo...
## Notas
...notas relevantes...
```
### Despues de crear
```bash
./fn index && ./fn show {id}
```
---
## CREAR FEATURE
Para features dentro de una app existente. Co-location obligatoria.
### Estructura
```
src/features/{feature_name}/
components/
FeatureMain.tsx # Componente principal
FeatureDetail.tsx # Sub-componentes
hooks/
useFeatureData.ts # Hooks del feature
types.ts # Tipos locales
index.ts # Barrel export
```
### Barrel export (index.ts)
```ts
// Solo exportar la API publica del feature
export { FeatureMain } from './components/FeatureMain'
export { useFeatureData } from './hooks/useFeatureData'
export type { FeatureItem, FeatureConfig } from './types'
```
### Patrones de estado obligatorios
**Server state** (datos de API/backend):
```tsx
// Con @tanstack/react-query
const queryKeys = {
all: ['feature'] as const,
list: (filters: Filters) => [...queryKeys.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.all, 'detail', id] as const,
}
function useFeatureList(filters: Filters) {
return useQuery({
queryKey: queryKeys.list(filters),
queryFn: () => fetchFeatureList(filters),
})
}
```
**Client state** (UI state compartido):
```tsx
// Con Zustand
import { create } from 'zustand'
interface FeatureStore {
selectedId: string | null
setSelected: (id: string | null) => void
}
const useFeatureStore = create<FeatureStore>((set) => ({
selectedId: null,
setSelected: (id) => set({ selectedId: id }),
}))
```
**Wails** (apps de escritorio):
```tsx
// Usar hooks del registry
import { useWailsQuery, useWailsMutation } from '@fn_library'
function useFeatureData() {
return useWailsQuery('GetFeatureData', [], { staleTime: 60_000 })
}
```
### Code splitting por ruta
```tsx
import { lazy, Suspense } from 'react'
import { Skeleton } from '@fn_library'
const FeaturePage = lazy(() => import('./features/feature/components/FeaturePage'))
function AppRoutes() {
return (
<Routes>
<Route path="/feature" element={
<Suspense fallback={<Skeleton className="h-screen w-full" />}>
<FeaturePage />
</Suspense>
} />
</Routes>
)
}
```
---
## CHECKLIST DE VALIDACION (ejecutar siempre al final)
Antes de dar por terminado cualquier trabajo frontend, verificar:
### Colores y estilos
- [ ] CERO colores hardcodeados (no hex, no rgb, no oklch inline en componentes)
- [ ] Solo clases Tailwind mapeadas a CSS variables: `bg-primary`, `text-foreground`, `border-border`, etc.
- [ ] `cn()` usado para merge de clases en todo componente
- [ ] CVA usado para variantes (no condicionales manuales con ternarios)
### Componentes del registry
- [ ] Verificado que no se esta recreando algo que ya existe en `@fn_library` (`frontend/functions/ui/`)
- [ ] Componentes de `@fn_library` usados donde aplica: Alert, Badge, Button, Card, Dialog, Input, Label, Select, SimpleSelect, Skeleton, Sparkline, Tabs, Tooltip, FormField, PageHeader, ProgressBar, KPICard, ThemeProvider, DashboardLayout, DataTable, charts (AreaChart, BarChart, LineChart, PieChart, ChartContainer), hooks Wails (useWailsQuery, useWailsMutation, useWailsStream, useWailsEvent)
### TypeScript
- [ ] Props interfaces con `React.ComponentPropsWithoutRef` para HTML spreading
- [ ] Discriminated unions donde las props varian segun tipo/variante
- [ ] `as const` para arrays literales y config objects
- [ ] No `any` — usar `unknown` + type guards si es necesario
### Accesibilidad
- [ ] Elementos semanticos (button, a, dialog — no div onClick)
- [ ] `aria-label` en botones de solo icono
- [ ] `aria-invalid` + `aria-describedby` en inputs con validacion
- [ ] Focus trap en modales y popovers
- [ ] `prefers-reduced-motion` respetado (ya en app.css base)
### Performance
- [ ] Lazy loading en rutas (`React.lazy` + `Suspense`)
- [ ] `manualChunks` en vite.config para vendor splitting
- [ ] Sin barrel exports profundos que maten tree-shaking
- [ ] Listas largas virtualizadas si >100 items
### Estructura
- [ ] Features co-located: componente + hook + tipos + barrel en el mismo directorio
- [ ] Un `index.ts` por feature con API publica explicita
- [ ] Componentes reutilizables de la app en `src/components/`
- [ ] Tipos compartidos en `src/types/`
---
## ANTI-PATRONES (nunca hacer)
1. **`<div onClick={...}>`** → usar `<button>` o Base-UI primitivo
2. **`style={{ color: '#3b82f6' }}`** → usar `className="text-primary"`
3. **`import Button from './MyButton'`** cuando existe en la lib → usar `import { Button } from '@fn_library'`
4. **Estado global para todo** → segmentar: server state (React Query), client state (Zustand), form state (React Hook Form), URL state (search params)
5. **`index.ts` en la raiz de `src/`** que re-exporta todo → mata tree-shaking
6. **`// @ts-ignore`** → arreglar el tipo
7. **CSS-in-JS runtime** (styled-components, emotion) → usar Tailwind
8. **Instalar shadcn/ui como dependencia** → los componentes ya estan en el registry, usar `@fn_library`
9. **Crear utilidades que ya existen**: `cn()`, `getSeriesColor()`, `ChartContainer`, `ThemeProvider` ya estan en `@fn_library`
10. **Colores de chart hardcodeados** → usar `--chart-1` a `--chart-5` o `getSeriesColor()`
$ARGUMENTS
+1
View File
@@ -16,3 +16,4 @@ Reglas operativas del proyecto. Cada archivo es una regla independiente.
| 10 | [apps_vs_functions.md](apps_vs_functions.md) | Codigo reutilizable en functions/, no reutilizable en apps/ |
| 11 | [sources.md](sources.md) | Extraccion de funciones desde repos externos |
| 12 | [notebook_collaboration.md](notebook_collaboration.md) | Colaboración en notebooks Jupyter via funciones del registry |
| 13 | [frontend_theming.md](frontend_theming.md) | Componentes propios y sistema de temas en frontends |
+3
View File
@@ -0,0 +1,3 @@
En todos los frontends se usan los componentes de `@fn_library` (alias a `frontend/functions/ui/`) antes que elementos HTML nativos o librerias externas.
En todos los frontends se usa el sistema de temas basado en CSS variables (`--background`, `--foreground`, `--input`, `--border`, `--popover`, etc.) definidas en `app.css`. Los componentes deben leer estas variables para adaptarse al tema activo. Nunca hardcodear colores.
@@ -0,0 +1,67 @@
---
name: audit_registry_paths
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "audit_registry_paths([output_file: string]) -> void"
description: "Audita file_path de todas las functions y types en registry.db, verifica que cada ruta apunte a un archivo existente en disco, y genera un txt con las rutas rotas para que agentes puedan corregirlas."
tags: [registry, audit, validation, paths, launcher, pipeline, bash]
uses_functions:
- assert_command_exists_bash_shell
- assert_file_exists_bash_shell
- validate_registry_paths_bash_shell
- report_execution_json_bash_shell
- exit_with_status_bash_shell
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/audit_registry_paths.sh"
---
## Ejemplo
```bash
# Con default (genera broken_paths.txt en la raiz)
./bash/functions/pipelines/audit_registry_paths.sh
# Con ruta personalizada
./bash/functions/pipelines/audit_registry_paths.sh /tmp/broken.txt
# Desde fn run (pipeline launcher)
fn run audit_registry_paths
```
## Flujo
1. `assert_command_exists` — verifica que `sqlite3` esta disponible
2. `assert_file_exists` — verifica que `registry.db` existe y reporta su tamano
3. `validate_registry_paths` (functions) — itera todas las functions, verifica cada file_path
4. `validate_registry_paths` (types) — itera todos los types, verifica cada file_path
5. Genera `broken_paths.txt` con formato legible para agentes
6. `report_execution_json` — imprime JSON de ejecucion a stdout
7. `exit_with_status` — exit code segun resultado
## Formato de salida
```
# Broken file_path entries in registry.db
# Generated: 2026-04-03T10:00:00Z
# Total: 11 broken paths
#
# Format: id | file_path (in .md) | domain | table
# ---
## Functions (11)
cdp_click_go_browser | functions/infra/cdp_click.go | browser | functions
```
## Notas
El archivo de salida es consumible por agentes: cada linea tiene el ID de la funcion/tipo y el file_path que necesita correccion. El agente puede leer el .md correspondiente, encontrar el archivo real en disco, y actualizar el campo `file_path`.
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# audit_registry_paths
# --------------------
# Audita file_path de functions y types en registry.db.
# Genera un txt con las rutas rotas para que agentes puedan arreglarlas.
#
# Compone: assert_command_exists + assert_file_exists +
# validate_registry_paths + report_execution_json + exit_with_status
#
# USO:
# ./audit_registry_paths.sh [OUTPUT_FILE]
#
# ARGUMENTOS (opcionales):
# OUTPUT_FILE Ruta del archivo de salida
# Default: $REGISTRY_ROOT/broken_paths.txt
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh"
source "$REGISTRY_ROOT/bash/functions/shell/validate_registry_paths.sh"
source "$REGISTRY_ROOT/bash/functions/shell/report_execution_json.sh"
source "$REGISTRY_ROOT/bash/functions/shell/exit_with_status.sh"
OUTPUT_FILE="${1:-$REGISTRY_ROOT/broken_paths.txt}"
DB_PATH="$REGISTRY_ROOT/registry.db"
STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
START_MS=$(date +%s%3N)
STEPS_FILE=$(mktemp)
trap 'rm -f "$STEPS_FILE"' EXIT
ok_steps=0
failed_steps=0
log_step() {
local name="$1" action="$2" status="$3" elapsed="$4" output="${5:-}" error="${6:-}"
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$name" "$action" "$status" "$elapsed" "$output" "$error" >> "$STEPS_FILE"
if [[ "$status" == "ok" ]]; then ok_steps=$((ok_steps + 1)); else failed_steps=$((failed_steps + 1)); fi
}
# Paso 1: verificar sqlite3
step_start=$(date +%s%3N)
if assert_command_exists sqlite3; then
log_step "assert_command_exists" "check sqlite3" "ok" $(( $(date +%s%3N) - step_start ))
else
log_step "assert_command_exists" "check sqlite3" "error" $(( $(date +%s%3N) - step_start )) "" "sqlite3 not found"
ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
DURATION=$(( $(date +%s%3N) - START_MS ))
set +e; report_execution_json "audit_registry_paths" "failure" 1 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"; set -e
exit 1
fi
# Paso 2: verificar registry.db
step_start=$(date +%s%3N)
if db_size=$(assert_file_exists "$DB_PATH"); then
log_step "assert_file_exists" "check registry.db" "ok" $(( $(date +%s%3N) - step_start )) "${db_size} bytes"
else
log_step "assert_file_exists" "check registry.db" "error" $(( $(date +%s%3N) - step_start )) "" "registry.db not found"
ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
DURATION=$(( $(date +%s%3N) - START_MS ))
set +e; report_execution_json "audit_registry_paths" "failure" 1 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"; set -e
exit 1
fi
# Paso 3: validar functions
step_start=$(date +%s%3N)
fn_broken=$(validate_registry_paths "$DB_PATH" functions "$REGISTRY_ROOT")
fn_count=$(printf '%s' "$fn_broken" | grep -c . || true)
log_step "validate_registry_paths" "check functions" "ok" $(( $(date +%s%3N) - step_start )) "$fn_count broken"
# Paso 4: validar types
step_start=$(date +%s%3N)
ty_broken=$(validate_registry_paths "$DB_PATH" types "$REGISTRY_ROOT")
ty_count=$(printf '%s' "$ty_broken" | grep -c . || true)
log_step "validate_registry_paths" "check types" "ok" $(( $(date +%s%3N) - step_start )) "$ty_count broken"
# Paso 5: generar archivo de salida
step_start=$(date +%s%3N)
total_broken=$(( fn_count + ty_count ))
{
echo "# Broken file_path entries in registry.db"
echo "# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "# Total: $total_broken broken paths"
echo "#"
echo "# Format: id | file_path (in .md) | domain | table"
echo "# ---"
if [[ $total_broken -eq 0 ]]; then
echo "# All paths are valid."
else
if [[ -n "$fn_broken" ]]; then
echo ""
echo "## Functions ($fn_count)"
printf '%s\n' "$fn_broken" | while IFS=$'\t' read -r id fp domain table; do
echo "$id | $fp | $domain | $table"
done
fi
if [[ -n "$ty_broken" ]]; then
echo ""
echo "## Types ($ty_count)"
printf '%s\n' "$ty_broken" | while IFS=$'\t' read -r id fp domain table; do
echo "$id | $fp | $domain | $table"
done
fi
fi
} > "$OUTPUT_FILE"
log_step "write_output" "generate $OUTPUT_FILE" "ok" $(( $(date +%s%3N) - step_start )) "$total_broken entries"
# Reporte final
ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
DURATION=$(( $(date +%s%3N) - START_MS ))
total_steps=$(( ok_steps + failed_steps ))
if [[ $total_broken -eq 0 ]]; then
status="success"
else
status="partial"
fi
set +e
report_execution_json "audit_registry_paths" "$status" 0 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"
set -e
echo ""
echo "--- Results ---"
echo "Broken paths: $total_broken"
echo "Output: $OUTPUT_FILE"
exit_with_status "$total_steps" "$ok_steps" "$failed_steps" > /dev/null
@@ -0,0 +1,37 @@
---
name: validate_registry_paths
kind: function
lang: bash
domain: shell
version: "1.0.0"
purity: impure
signature: "validate_registry_paths(db_path: string, table: string, root_dir: string) -> tsv_stdout"
description: "Consulta registry.db y verifica que cada file_path apunte a un archivo existente en disco. Imprime a stdout las rutas rotas en formato TSV (id, file_path, domain, tabla)."
tags: [registry, validation, paths, audit, bash]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/shell/validate_registry_paths.sh"
---
## Ejemplo
```bash
source validate_registry_paths.sh
validate_registry_paths /home/lucas/fn_registry/registry.db functions /home/lucas/fn_registry
# Output (TSV):
# cdp_click_go_browser functions/infra/cdp_click.go browser functions
```
## Notas
Impura porque lee el filesystem y la base de datos. No modifica nada — solo reporta.
La salida TSV es consumible por otros scripts o pipelines. Si no hay rutas rotas, no imprime nada.
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# validate_registry_paths
# -----------------------
# Consulta registry.db y verifica que cada file_path apunte a un archivo existente.
# Recibe la ruta a registry.db y la tabla a validar (functions o types).
# Imprime a stdout las lineas con rutas rotas en formato TSV:
# id<TAB>file_path<TAB>domain<TAB>tabla
# Exit code 0 siempre (es una consulta, no una asercion).
#
# USO (sourced):
# source validate_registry_paths.sh
# validate_registry_paths /ruta/registry.db functions /ruta/raiz
#
# USO (directo):
# bash validate_registry_paths.sh /ruta/registry.db functions /ruta/raiz
validate_registry_paths() {
local db_path="$1"
local table="$2"
local root_dir="$3"
if [[ -z "$db_path" || -z "$table" || -z "$root_dir" ]]; then
echo "validate_registry_paths: uso: validate_registry_paths <db_path> <table> <root_dir>" >&2
return 1
fi
if [[ "$table" != "functions" && "$table" != "types" ]]; then
echo "validate_registry_paths: tabla debe ser 'functions' o 'types'" >&2
return 1
fi
local broken=0
while IFS='|' read -r id fp domain; do
if [[ ! -f "$root_dir/$fp" ]]; then
printf '%s\t%s\t%s\t%s\n' "$id" "$fp" "$domain" "$table"
((broken++))
fi
done < <(sqlite3 "$db_path" "SELECT id, file_path, domain FROM $table ORDER BY id;")
return 0
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
validate_registry_paths "$@"
fi
+3
View File
@@ -127,6 +127,9 @@ func cmdIndex() {
for _, e := range result.ValidationErrors {
fmt.Fprintf(os.Stderr, " INVALID: %s\n", e)
}
for _, w := range result.Warnings {
fmt.Fprintf(os.Stderr, " WARN: %s\n", w)
}
for _, e := range result.Errors {
fmt.Fprintf(os.Stderr, " ERROR: %s\n", e)
}
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: chart_colors
kind: function
lang: typescript
lang: ts
domain: core
version: "1.0.0"
purity: pure
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: cn
kind: function
lang: typescript
lang: ts
domain: core
version: "1.0.0"
purity: pure
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: format_compact
kind: function
lang: typescript
lang: ts
domain: core
version: "1.0.0"
purity: pure
@@ -0,0 +1,63 @@
---
name: generate_theme_css
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: pure
signature: "generateThemeCss(colors: Record<string, string>, selector?: string): string"
description: "Genera un bloque CSS con variables de tema a partir de un objeto de tokens. Convierte claves camelCase a kebab-case automaticamente. Pura — solo transforma datos, no accede al DOM."
tags: [theme, css, generator, pure]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/generate_theme_css.ts"
---
## Ejemplo
```typescript
import { generateThemeCss } from './generate_theme_css'
import { themeConfigToColors } from './theme_config_to_colors'
import { darkTheme } from '../ui/themes'
// Generar CSS para inyectar en <style>
const colors = themeConfigToColors(darkTheme)
const css = generateThemeCss(colors)
// Output:
// :root {
// --background: oklch(8% 0.015 260);
// --foreground: oklch(95% 0.01 260);
// --card: oklch(12% 0.015 260);
// --card-foreground: oklch(95% 0.01 260);
// ...
// }
// Inyectar en el documento
const style = document.createElement('style')
style.textContent = css
document.head.appendChild(style)
// Generar para selector especifico (dark mode)
const darkCss = generateThemeCss(colors, '.dark')
// Output: .dark { --background: oklch(...); ... }
// Generar para multiples selectores
const lightCss = generateThemeCss(lightColors, ':root')
const darkCss2 = generateThemeCss(darkColors, ':root.dark')
const combined = [lightCss, darkCss2].join('\n\n')
```
## Notas
Funcion pura — sin acceso al DOM, sin side effects. Util para SSR, generacion de archivos CSS estaticos, o pre-generar temas en build time.
La conversion camelCase → kebab-case es simple (reemplaza mayusculas con `-` + minuscula). No maneja casos especiales como `backgroundColor``background-color`; los tokens del registry ya usan nombres semanticos directos (`background`, `cardForeground`, etc.).
Compone naturalmente con `themeConfigToColors` del registry: `generateThemeCss(themeConfigToColors(config))`.
@@ -0,0 +1,23 @@
/**
* Genera un bloque CSS con variables de tema para inyectar como &lt;style&gt; o
* escribir en un archivo .css.
*
* Convierte claves camelCase a kebab-case automaticamente:
* `cardForeground` → `--card-foreground`
*
* @param colors - Objeto con tokens de tema. Claves en camelCase, valores CSS.
* @param selector - Selector CSS donde aplicar las variables. Por defecto `:root`.
* @returns String CSS con el bloque completo.
*/
export function generateThemeCss(
colors: Record<string, string>,
selector: string = ':root',
): string {
const lines = Object.entries(colors)
.map(([key, value]) => {
const cssName = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
return ` --${cssName}: ${value};`
})
.join('\n')
return `${selector} {\n${lines}\n}`
}
@@ -0,0 +1,49 @@
---
name: get_computed_color
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: impure
signature: "getComputedColor(cssVar: string): string"
description: "Resuelve una CSS variable de color a su valor RGB computado por el browser. Acepta '--primary', 'primary' o 'var(--primary)'. Util para canvas, sigma.js y APIs que no soportan CSS variables."
tags: [theme, css, color, runtime, dom, canvas]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/get_computed_color.ts"
---
## Ejemplo
```typescript
import { getComputedColor } from './get_computed_color'
// Todos estos formatos son equivalentes:
const color1 = getComputedColor('--primary') // "rgb(120, 80, 220)"
const color2 = getComputedColor('primary') // "rgb(120, 80, 220)"
const color3 = getComputedColor('var(--primary)') // "rgb(120, 80, 220)"
// Usar en canvas 2D
const ctx = canvas.getContext('2d')
ctx.fillStyle = getComputedColor('--background')
ctx.strokeStyle = getComputedColor('--border')
// Usar en sigma.js v2
renderer.setSetting('defaultNodeColor', getComputedColor('--primary'))
renderer.setSetting('defaultEdgeColor', getComputedColor('--muted'))
```
## Notas
Impura — muta el DOM temporalmente (append + remove) para forzar la resolucion del color. Solo disponible en browser.
El browser retorna el valor en formato `rgb(r, g, b)` o `rgba(r, g, b, a)`, no en el formato original de la CSS variable (oklch, hsl, etc.). Si necesitas el valor original sin resolver, usar `getThemeTokens` en su lugar.
Para multiples colores en un mismo render, llamar una vez y cachear los resultados — cada llamada hace un ciclo DOM.
@@ -0,0 +1,27 @@
/**
* Resuelve una CSS variable de color a su valor RGB computado por el browser.
*
* Acepta cualquiera de los formatos:
* - `"--primary"`
* - `"primary"` (sin prefijo --)
* - `"var(--primary)"`
*
* Retorna el valor en formato `rgb(r, g, b)` o `rgba(r, g, b, a)` segun
* como lo expanda el browser. Util para canvas, sigma.js y cualquier API
* que no soporte CSS variables directamente.
*/
export function getComputedColor(cssVar: string): string {
const name = cssVar.startsWith('var(')
? cssVar.slice(4, -1).trim()
: cssVar.startsWith('--')
? cssVar
: `--${cssVar}`
const el = document.createElement('div')
el.style.color = `var(${name})`
el.style.display = 'none'
document.body.appendChild(el)
const computed = getComputedStyle(el).color
document.body.removeChild(el)
return computed
}
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: get_series_color
kind: function
lang: typescript
lang: ts
domain: core
version: "1.0.0"
purity: pure
@@ -0,0 +1,49 @@
---
name: get_theme_tokens
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: impure
signature: "getThemeTokens(): ThemeTokens"
description: "Lee todas las CSS variables de tema del documento y devuelve un objeto tipado con los valores computados desde :root. Util para pasar colores a APIs que no entienden CSS variables (canvas, sigma.js, D3)."
tags: [theme, css, tokens, runtime, dom]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/get_theme_tokens.ts"
---
## Ejemplo
```typescript
import { getThemeTokens } from './get_theme_tokens'
const tokens = getThemeTokens()
// Pasar colores a sigma.js (que no soporta CSS variables)
const sigmaSettings = {
defaultNodeColor: tokens.primary,
defaultEdgeColor: tokens.muted,
labelColor: { color: tokens.foreground },
}
// Pasar colores a un canvas 2D
const ctx = canvas.getContext('2d')
ctx.fillStyle = tokens.background
ctx.strokeStyle = tokens.border
```
## Notas
Impura — accede a `document.documentElement` y `getComputedStyle`. Solo disponible en browser.
Los valores retornados son los valores sin procesar de las CSS variables (ej: `oklch(8% 0.015 260)`). Para obtener valores RGB computed (necesarios para algunas APIs), usar `getComputedColor`.
Funciona con cualquier tema activo: el resultado cambia automaticamente cuando se cambia el tema via `applyTheme`.
@@ -0,0 +1,59 @@
/** Tokens de tema leidos de las CSS variables activas en :root. */
export interface ThemeTokens {
background: string
foreground: string
card: string
cardForeground: string
popover: string
popoverForeground: string
primary: string
primaryForeground: string
secondary: string
secondaryForeground: string
muted: string
mutedForeground: string
accent: string
accentForeground: string
destructive: string
destructiveForeground: string
success: string
successForeground: string
border: string
input: string
ring: string
}
/**
* Lee todas las CSS variables de tema del documento y devuelve un objeto
* tipado con los valores computados desde :root.
*
* Util para pasar colores a APIs que no entienden CSS variables
* (canvas, sigma.js, D3, etc.).
*/
export function getThemeTokens(): ThemeTokens {
const style = getComputedStyle(document.documentElement)
const get = (name: string) => style.getPropertyValue(`--${name}`).trim()
return {
background: get('background'),
foreground: get('foreground'),
card: get('card'),
cardForeground: get('card-foreground'),
popover: get('popover'),
popoverForeground: get('popover-foreground'),
primary: get('primary'),
primaryForeground: get('primary-foreground'),
secondary: get('secondary'),
secondaryForeground: get('secondary-foreground'),
muted: get('muted'),
mutedForeground: get('muted-foreground'),
accent: get('accent'),
accentForeground: get('accent-foreground'),
destructive: get('destructive'),
destructiveForeground: get('destructive-foreground'),
success: get('success'),
successForeground: get('success-foreground'),
border: get('border'),
input: get('input'),
ring: get('ring'),
}
}
@@ -1,7 +1,7 @@
---
name: theme_config_to_colors
kind: function
lang: typescript
lang: ts
domain: core
version: "1.0.0"
purity: pure
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: wails_cache
kind: function
lang: typescript
lang: ts
domain: core
version: "1.0.0"
purity: pure
+53
View File
@@ -0,0 +1,53 @@
---
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. Base-UI Collapsible primitive. Composable: AccordionItem + AccordionTrigger + AccordionContent."
tags: [accordion, collapsible, component, ui, interactive, base-ui]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react/collapsible", "lucide-react"]
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
<Accordion>
<AccordionItem defaultOpen>
<AccordionTrigger>Seccion 1</AccordionTrigger>
<AccordionContent>
Contenido de la primera seccion.
</AccordionContent>
</AccordionItem>
<AccordionItem>
<AccordionTrigger>Seccion 2</AccordionTrigger>
<AccordionContent>
Contenido de la segunda seccion.
</AccordionContent>
</AccordionItem>
</Accordion>
```
## Notas
Cada AccordionItem es un Collapsible independiente — permite multiples items abiertos simultaneamente. Para exclusividad (solo uno abierto), manejar el estado externamente. El chevron rota 180 grados con [data-open]. Exports: Accordion, AccordionItem, AccordionTrigger, AccordionContent.
+81
View File
@@ -0,0 +1,81 @@
import * as React from "react"
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "../core/cn"
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, children, ...props }: React.ComponentProps<"div"> & AccordionProps) {
return (
<div data-slot="accordion" className={cn("divide-y divide-border", className)} {...props}>
{children}
</div>
)
}
interface AccordionItemProps extends CollapsiblePrimitive.Root.Props {
className?: string
}
function AccordionItem({ className, ...props }: AccordionItemProps) {
return (
<CollapsiblePrimitive.Root
data-slot="accordion-item"
className={cn("group/accordion-item", className)}
{...props}
/>
)
}
function AccordionTrigger({ className, children, ...props }: CollapsiblePrimitive.Trigger.Props) {
return (
<CollapsiblePrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"flex w-full items-center justify-between py-4 text-sm font-medium transition-all outline-none",
"hover:underline focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:underline",
"disabled:pointer-events-none disabled:opacity-50",
"[&[data-open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</CollapsiblePrimitive.Trigger>
)
}
function AccordionContent({ className, children, ...props }: CollapsiblePrimitive.Panel.Props) {
return (
<CollapsiblePrimitive.Panel
data-slot="accordion-content"
className={cn(
"overflow-hidden text-sm",
"data-open:animate-in data-open:fade-in-0",
"data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
>
<div className="pb-4">{children}</div>
</CollapsiblePrimitive.Panel>
)
}
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
export type { AccordionItem as AccordionItemData, AccordionProps }
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: alert
kind: component
lang: typescript
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. Sistema de slots para título, descripción, icono y acción."
tags: [alert, feedback, component, ui, notification]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: analytics_page
kind: function
lang: typescript
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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+2 -2
View File
@@ -1,7 +1,7 @@
---
name: apply_theme
kind: function
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
@@ -9,7 +9,7 @@ signature: "applyTheme(theme: Theme): void"
description: "Inyecta un tema como CSS variables en document.documentElement. Maneja clase dark automáticamente. Mapea 40 tokens semánticos."
tags: [theme, css-variables, apply, runtime, ui]
uses_functions: []
uses_types: [ThemeConfig_typescript_ui]
uses_types: [ThemeConfig_ts_ui]
returns: []
returns_optional: false
error_type: "error_go_core"
+3 -3
View File
@@ -1,15 +1,15 @@
---
name: area_chart
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "AreaChart(props: AreaChartProps): JSX.Element"
description: "Gráfico de área Recharts con gradientes automáticos, multi-series, stacking y tooltips temáticos."
tags: [chart, area, visualization, recharts, gradient, component, ui]
uses_functions: [cn_typescript_core, chart_container_typescript_ui, get_series_color_typescript_core]
uses_types: [ChartSeries_typescript_ui]
uses_functions: [cn_ts_core, chart_container_ts_ui, get_series_color_ts_core]
uses_types: [ChartSeries_ts_ui]
returns: []
returns_optional: false
error_type: ""
+70
View File
@@ -0,0 +1,70 @@
---
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 CVA."
tags: [avatar, user, image, component, ui, cva]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["class-variance-authority"]
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: true
framework: react
variant: [xs, sm, md, lg, xl]
---
## Ejemplo
```tsx
// Con imagen
<Avatar src="https://example.com/user.jpg" alt="Juan Perez" size="md" />
// Con fallback a iniciales
<Avatar fallback="Juan Perez" size="lg" />
// Iniciales explicitas
<Avatar initials="JD" size="sm" />
// Maneja error de imagen automaticamente
<Avatar src="/broken-url.jpg" fallback="Maria Garcia" />
```
## Notas
Usa estado interno para manejar errores de carga de imagen (onError). 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.
+69
View File
@@ -0,0 +1,69 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
const avatarVariants = cva(
"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground select-none",
{
variants: {
size: {
xs: "size-6 text-xs",
sm: "size-8 text-sm",
md: "size-10 text-base",
lg: "size-12 text-lg",
xl: "size-16 text-xl",
},
},
defaultVariants: { size: "md" },
}
)
interface AvatarProps
extends React.ComponentPropsWithoutRef<"span">,
VariantProps<typeof avatarVariants> {
src?: string
alt?: string
fallback?: string
initials?: string
}
function getInitials(name?: string): string {
if (!name) return "?"
const parts = name.trim().split(/\s+/)
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
}
const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
({ className, size, src, alt, fallback, initials, ...props }, ref) => {
const [imgError, setImgError] = React.useState(false)
const showImage = src && !imgError
const displayInitials = initials ?? getInitials(fallback ?? alt)
return (
<span
ref={ref}
data-slot="avatar"
className={cn(avatarVariants({ size }), className)}
{...props}
>
{showImage ? (
<img
src={src}
alt={alt ?? ""}
className="aspect-square size-full object-cover"
onError={() => setImgError(true)}
/>
) : (
<span data-slot="avatar-fallback" aria-hidden="true">
{displayInitials}
</span>
)}
</span>
)
}
)
Avatar.displayName = "Avatar"
export { Avatar, avatarVariants }
export type { AvatarProps }
+1 -1
View File
@@ -8,7 +8,7 @@ purity: impure
signature: "Badge(props: BadgeProps & VariantProps<typeof badgeVariants>): JSX.Element"
description: "Badge con 10 variantes semánticas (default, secondary, destructive, outline, ghost, link, success, warning, error, info) y 2 tamaños."
tags: [badge, status, component, ui, indicator]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+3 -3
View File
@@ -1,15 +1,15 @@
---
name: bar_chart
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.1.0"
purity: impure
signature: "BarChart(props: BarChartProps): JSX.Element"
description: "Gráfico de barras Recharts con multi-series, orientación horizontal/vertical, tooltips temáticos y bordes redondeados."
tags: [chart, bar, visualization, recharts, component, ui]
uses_functions: [cn_typescript_core, chart_container_typescript_ui, get_series_color_typescript_core]
uses_types: [ChartSeries_typescript_ui]
uses_functions: [cn_ts_core, chart_container_ts_ui, get_series_color_ts_core]
uses_types: [ChartSeries_ts_ui]
returns: []
returns_optional: false
error_type: ""
+71
View File
@@ -0,0 +1,71 @@
---
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."
tags: [breadcrumb, navigation, component, ui]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["lucide-react"]
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
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Inicio</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href="/docs">Documentacion</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Componentes</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
// Con elipsis para paths largos
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Inicio</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Pagina actual</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
```
## Notas
Exports: Breadcrumb (nav), BreadcrumbList (ol), BreadcrumbItem (li), BreadcrumbLink (a con asChild), BreadcrumbPage (span aria-current=page), BreadcrumbSeparator (ChevronRight por defecto, customizable), BreadcrumbEllipsis (MoreHorizontal). BreadcrumbLink acepta asChild para usar con Link de React Router o Next.js.
+97
View File
@@ -0,0 +1,97 @@
import * as React from "react"
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
import { cn } from "../core/cn"
function Breadcrumb({ ...props }: React.ComponentPropsWithoutRef<"nav">) {
return <nav data-slot="breadcrumb" aria-label="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentPropsWithoutRef<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn("flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", className)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentPropsWithoutRef<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
className,
href,
asChild,
children,
...props
}: React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }) {
if (asChild) {
return (
<span data-slot="breadcrumb-link" className={cn("transition-colors hover:text-foreground", className)} {...(props as React.ComponentPropsWithoutRef<"span">)}>
{children}
</span>
)
}
return (
<a
data-slot="breadcrumb-link"
href={href}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
>
{children}
</a>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-current="page"
aria-disabled="true"
className={cn("font-medium text-foreground", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRightIcon />}
</li>
)
}
function BreadcrumbEllipsis({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator }
+1 -1
View File
@@ -8,7 +8,7 @@ purity: impure
signature: "Button(props: ButtonProps & VariantProps<typeof buttonVariants>): JSX.Element"
description: "Botón accesible con 6 variantes (default, outline, secondary, ghost, destructive, link) y 8 tamaños. Base-UI primitivo con CVA."
tags: [button, component, ui, interactive, cva]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+1 -1
View File
@@ -8,7 +8,7 @@ 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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+3 -3
View File
@@ -1,15 +1,15 @@
---
name: chart_container
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "ChartContainer(props: { children: ReactNode; height?: number | string }): JSX.Element"
description: "Base para todos los charts Recharts: container responsive, tooltip temático, legend y utilidades de colores por serie."
tags: [chart, container, recharts, base, visualization, component, ui]
uses_functions: [cn_typescript_core, get_series_color_typescript_core]
uses_types: [ChartSeries_typescript_ui]
uses_functions: [cn_ts_core, get_series_color_ts_core]
uses_types: [ChartSeries_ts_ui]
returns: []
returns_optional: false
error_type: ""
+72
View File
@@ -0,0 +1,72 @@
---
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. Base-UI Checkbox primitive."
tags: [checkbox, component, ui, interactive, form, base-ui]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react/checkbox", "class-variance-authority"]
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
<Checkbox label="Acepto los terminos" />
// Controlado
<Checkbox
label="Seleccionar todos"
checked={allSelected}
indeterminate={someSelected}
onCheckedChange={setAllSelected}
/>
// Sin label
<Checkbox checked={isActive} onCheckedChange={setIsActive} />
```
## Notas
Usa Base-UI Checkbox primitive para accesibilidad completa (keyboard, ARIA). El estado indeterminate se muestra con un guion horizontal. El id se genera automaticamente con useId si no se provee.
+79
View File
@@ -0,0 +1,79 @@
import * as React from "react"
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { CheckboxIndicator } from "@base-ui/react/checkbox"
import { cn } from "../core/cn"
interface CheckboxProps extends CheckboxPrimitive.Root.Props {
label?: string
indeterminate?: boolean
className?: string
labelClassName?: string
}
function Checkbox({ className, label, id, indeterminate, ...props }: CheckboxProps) {
const internalId = React.useId()
const checkboxId = id ?? internalId
return (
<div className="flex items-center gap-2">
<CheckboxPrimitive.Root
id={checkboxId}
data-slot="checkbox"
className={cn(
"peer size-4 shrink-0 rounded border border-input bg-transparent transition-colors outline-none",
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground",
"data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground",
"disabled:pointer-events-none disabled:opacity-50",
className
)}
indeterminate={indeterminate}
{...props}
>
<CheckboxIndicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current"
>
{indeterminate ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
className="size-3"
>
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
className="size-3"
>
<polyline points="20 6 9 17 4 12" />
</svg>
)}
</CheckboxIndicator>
</CheckboxPrimitive.Root>
{label && (
<label
htmlFor={checkboxId}
data-slot="checkbox-label"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 cursor-pointer select-none"
>
{label}
</label>
)}
</div>
)
}
export { Checkbox }
export type { CheckboxProps }
+81
View File
@@ -0,0 +1,81 @@
---
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: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["lucide-react"]
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" },
]
<CommandSearch
items={items}
placeholder="Buscar tecnologia..."
onValueChange={(val) => console.log(val)}
/>
// Composable para mayor control
<Command>
<CommandInput placeholder="Buscar..." value={query} onChange={(e) => setQuery(e.target.value)} />
<CommandList>
<CommandEmpty>Sin resultados.</CommandEmpty>
<CommandGroup heading="Sugerencias">
<CommandItem selected={selected === "1"} onSelect={() => setSelected("1")}>
Opcion 1
<CommandShortcut>K</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
```
## 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.
+204
View File
@@ -0,0 +1,204 @@
import * as React from "react"
import { SearchIcon, XIcon } from "lucide-react"
import { cn } from "../core/cn"
interface CommandItem {
value: string
label: string
description?: string
icon?: React.ReactNode
disabled?: boolean
group?: string
}
interface CommandProps {
items: CommandItem[]
value?: string
onValueChange?: (value: string) => void
placeholder?: string
emptyMessage?: string
className?: string
inputClassName?: string
listClassName?: string
}
function Command({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
<div
data-slot="command"
className={cn("flex h-full w-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground", className)}
{...props}
/>
)
}
function CommandInput({ className, ...props }: React.ComponentPropsWithoutRef<"input">) {
return (
<div data-slot="command-input-wrapper" className="flex items-center border-b px-3">
<SearchIcon className="mr-2 size-4 shrink-0 text-muted-foreground" />
<input
data-slot="command-input"
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none",
"placeholder:text-muted-foreground",
"disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
<div
data-slot="command-list"
className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
{...props}
/>
)
}
function CommandEmpty({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
<div
data-slot="command-empty"
className={cn("py-6 text-center text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CommandGroup({ className, heading, ...props }: React.ComponentPropsWithoutRef<"div"> & { heading?: string }) {
return (
<div data-slot="command-group" className={cn("overflow-hidden p-1 text-foreground", className)}>
{heading && (
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">{heading}</div>
)}
<div {...props} />
</div>
)
}
function CommandSeparator({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
return (
<div
data-slot="command-separator"
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
)
}
interface CommandItemProps extends React.ComponentPropsWithoutRef<"div"> {
selected?: boolean
disabled?: boolean
onSelect?: () => void
}
function CommandItem({ className, selected, disabled, onSelect, ...props }: CommandItemProps) {
return (
<div
data-slot="command-item"
data-selected={selected}
aria-disabled={disabled}
role="option"
aria-selected={selected}
onClick={!disabled ? onSelect : undefined}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
"hover:bg-accent hover:text-accent-foreground",
disabled && "pointer-events-none opacity-50",
className
)}
{...props}
/>
)
}
function CommandShortcut({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
)
}
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<string, CommandItem[]>()
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 (
<Command className={className}>
<CommandInput
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
/>
<CommandList>
{filtered.length === 0 ? (
<CommandEmpty>{emptyMessage}</CommandEmpty>
) : (
Array.from(groups.entries()).map(([group, groupItems]) => (
<CommandGroup key={group} heading={group || undefined}>
{groupItems.map((item) => (
<CommandItem
key={item.value}
selected={selectedValue === item.value}
disabled={item.disabled}
onSelect={() => handleSelect(item.value)}
>
{item.icon && <span className="shrink-0">{item.icon}</span>}
<span>{item.label}</span>
{item.description && (
<span className="ml-auto text-xs text-muted-foreground">{item.description}</span>
)}
</CommandItem>
))}
</CommandGroup>
))
)}
</CommandList>
</Command>
)
}
export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut }
export type { CommandItem, CommandProps }
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: crud_page
kind: function
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: pure
signature: "crudPage<T>(props: CrudPageProps<T>): 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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: dashboard_layout
kind: function
lang: typescript
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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: data_table
kind: component
lang: typescript
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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: detail_page
kind: function
lang: typescript
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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: dialog
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Dialog(props: DialogRootProps): JSX.Element"
description: "Diálogo modal accesible con overlay blur, animaciones, close button y sistema de slots (header, footer, title, description)."
tags: [dialog, modal, overlay, component, ui, interactive]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+73
View File
@@ -0,0 +1,73 @@
---
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, base-ui]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react/menu", "lucide-react"]
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
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Acciones</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Mi cuenta</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onActivate={() => console.log("Perfil")}>
Perfil
</DropdownMenuItem>
<DropdownMenuCheckboxItem checked={showBookmarks} onCheckedChange={setShowBookmarks}>
Marcadores
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Mas opciones</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Opcion A</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
```
## Notas
Exports: DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuPortal.
+201
View File
@@ -0,0 +1,201 @@
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "../core/cn"
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
}
function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
}
function DropdownMenuContent({ className, sideOffset = 4, ...props }: MenuPrimitive.Positioner.Props) {
return (
<DropdownMenuPortal>
<MenuPrimitive.Positioner
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className="z-50"
{...props}
>
<MenuPrimitive.Popup
className={cn(
"min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
"data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
>
{props.children}
</MenuPrimitive.Popup>
</MenuPrimitive.Positioner>
</DropdownMenuPortal>
)
}
function DropdownMenuItem({ className, inset, ...props }: MenuPrimitive.Item.Props & { inset?: boolean }) {
return (
<MenuPrimitive.Item
data-slot="dropdown-menu-item"
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors",
"focus:bg-accent focus:text-accent-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({ className, children, checked, ...props }: MenuPrimitive.CheckboxItem.Props) {
return (
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"relative flex cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none transition-colors",
"focus:bg-accent focus:text-accent-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex size-4 items-center justify-center">
<MenuPrimitive.CheckboxItemIndicator>
<CheckIcon className="size-4" />
</MenuPrimitive.CheckboxItemIndicator>
</span>
{children}
</MenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
}
function DropdownMenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) {
return (
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"relative flex cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none transition-colors",
"focus:bg-accent focus:text-accent-foreground",
"data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex size-4 items-center justify-center">
<MenuPrimitive.RadioItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenuPrimitive.RadioItemIndicator>
</span>
{children}
</MenuPrimitive.RadioItem>
)
}
function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
}
function DropdownMenuLabel({ className, inset, ...props }: MenuPrimitive.GroupLabel.Props & { inset?: boolean }) {
return (
<MenuPrimitive.GroupLabel
data-slot="dropdown-menu-label"
className={cn("px-2 py-1.5 text-xs font-medium text-muted-foreground", inset && "pl-8", className)}
{...props}
/>
)
}
function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dropdown-menu-separator"
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
{...props}
/>
)
}
function DropdownMenuSub({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({ className, inset, children, ...props }: MenuPrimitive.SubmenuTrigger.Props & { inset?: boolean }) {
return (
<MenuPrimitive.SubmenuTrigger
data-slot="dropdown-menu-sub-trigger"
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors",
"focus:bg-accent focus:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground",
"[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</MenuPrimitive.SubmenuTrigger>
)
}
function DropdownMenuSubContent({ className, ...props }: MenuPrimitive.Positioner.Props) {
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner data-slot="dropdown-menu-sub-content" className="z-50" {...props}>
<MenuPrimitive.Popup
className={cn(
"min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
"data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
>
{props.children}
</MenuPrimitive.Popup>
</MenuPrimitive.Positioner>
</MenuPrimitive.Portal>
)
}
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
}
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: form_field
kind: component
lang: typescript
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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+121
View File
@@ -0,0 +1,121 @@
// 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, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectPortal, SelectSeparator, SelectTrigger, SelectValue } 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'
// Theme
export { ThemeProvider, useTheme, ThemeContext } from './theme_provider'
export type { ThemeProviderProps } from './theme_provider'
export { applyTheme } from './apply_theme'
export type { Theme, ThemeColors } from './apply_theme'
// 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 { 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, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from './pagination'
export type { PaginationLinkProps } 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'
// Hooks — Canvas
export { useAnimatedCanvas } from './use_animated_canvas'
// Wails Provider
export { WailsProvider } from './wails_provider'
+1 -1
View File
@@ -8,7 +8,7 @@ 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."
tags: [input, form, component, ui, interactive]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+3 -3
View File
@@ -1,14 +1,14 @@
---
name: kpi_card
kind: component
lang: typescript
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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
@@ -63,7 +63,7 @@ source_file: "frontend/src/components/ui/kpi-card.tsx"
## Ejemplo
```tsx
import { KPICard, Sparkline } from '@anthropic/frontend-lib'
import { KPICard, Sparkline } from '@fn_library'
{/* Básico */}
<KPICard label="Revenue" value="$12,450" delta={{ value: 12.5, isPositive: true }} />
+1 -1
View File
@@ -8,7 +8,7 @@ purity: impure
signature: "Label(props: LabelHTMLAttributes): JSX.Element"
description: "Etiqueta de formulario accesible con soporte para estados disabled y peer-disabled."
tags: [label, form, component, ui]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+3 -3
View File
@@ -1,15 +1,15 @@
---
name: line_chart
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "LineChart(props: LineChartProps): JSX.Element"
description: "Gráfico de líneas Recharts con multi-series, 5 tipos de curva, zoom brush, líneas de referencia, tooltips temáticos."
tags: [chart, line, visualization, recharts, component, ui]
uses_functions: [cn_typescript_core, chart_container_typescript_ui, get_series_color_typescript_core]
uses_types: [ChartSeries_typescript_ui]
uses_functions: [cn_ts_core, chart_container_ts_ui, get_series_color_ts_core]
uses_types: [ChartSeries_ts_ui]
returns: []
returns_optional: false
error_type: ""
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: page_header
kind: component
lang: typescript
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: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+61
View File
@@ -0,0 +1,61 @@
---
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 con Previous/Next, numeros de pagina, elipsis y estado activo."
tags: [pagination, navigation, component, ui]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["lucide-react", "./button"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/pagination.tsx"
props:
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: []
---
## Ejemplo
```tsx
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href="/page/1" />
</PaginationItem>
<PaginationItem>
<PaginationLink href="/page/1">1</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="/page/2" isActive>2</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationLink href="/page/3">3</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationNext href="/page/3" />
</PaginationItem>
</PaginationContent>
</Pagination>
```
## Notas
Exports: Pagination (nav), PaginationContent (ul), PaginationItem (li), PaginationLink (a con isActive/disabled), PaginationPrevious, PaginationNext, PaginationEllipsis. PaginationLink reutiliza buttonVariants para consistencia visual. Componente presentacional — el manejo del estado de pagina queda en el consumidor.
+100
View File
@@ -0,0 +1,100 @@
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
import { cn } from "../core/cn"
import { buttonVariants } from "./button"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
data-slot="pagination"
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
}
function PaginationContent({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
disabled?: boolean
size?: "icon" | "default" | "sm" | "lg"
} & React.ComponentProps<"a">
function PaginationLink({ className, isActive, disabled, size = "icon", ...props }: PaginationLinkProps) {
return (
<a
data-slot="pagination-link"
aria-current={isActive ? "page" : undefined}
aria-disabled={disabled}
className={cn(
buttonVariants({ variant: isActive ? "outline" : "ghost", size }),
disabled && "pointer-events-none opacity-50",
isActive && "border-border font-medium",
className
)}
{...props}
/>
)
}
function PaginationPrevious({ className, ...props }: React.ComponentProps<"a">) {
return (
<PaginationLink
data-slot="pagination-previous"
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 pl-2", className)}
{...props}
>
<ChevronLeftIcon className="size-4" />
<span>Previous</span>
</PaginationLink>
)
}
function PaginationNext({ className, ...props }: React.ComponentProps<"a">) {
return (
<PaginationLink
data-slot="pagination-next"
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 pr-2", className)}
{...props}
>
<span>Next</span>
<ChevronRightIcon className="size-4" />
</PaginationLink>
)
}
function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="pagination-ellipsis"
aria-hidden
className={cn("flex size-8 items-center justify-center text-muted-foreground", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
)
}
export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious }
export type { PaginationLinkProps }
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: pie_chart
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "PieChart(props: PieChartProps): JSX.Element"
description: "Gráfico de torta/dona Recharts con Cell por segmento, colores automáticos, labels con porcentaje, Legend y Tooltip temático. Soporte donut con innerRadius configurable."
tags: [chart, pie, donut, visualization, recharts, component, ui, dashboard]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+65
View File
@@ -0,0 +1,65 @@
---
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. Base-UI Popover primitive."
tags: [popover, component, ui, interactive, overlay, base-ui]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react/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
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Abrir</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverHeader>
<PopoverTitle>Configuracion</PopoverTitle>
<PopoverDescription>Ajusta tus preferencias.</PopoverDescription>
</PopoverHeader>
<div className="mt-4">
{/* contenido */}
</div>
</PopoverContent>
</Popover>
```
## Notas
Compuesto de: Popover (root), PopoverTrigger, PopoverContent (positioner + popup), PopoverClose, PopoverHeader, PopoverTitle, PopoverDescription. El posicionamiento automatico lo maneja Base-UI. Animaciones con data-open/data-closed.
+57
View File
@@ -0,0 +1,57 @@
import * as React from "react"
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
import { cn } from "../core/cn"
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverPortal({ ...props }: PopoverPrimitive.Portal.Props) {
return <PopoverPrimitive.Portal data-slot="popover-portal" {...props} />
}
function PopoverContent({ className, sideOffset = 4, ...props }: PopoverPrimitive.Positioner.Props) {
return (
<PopoverPortal>
<PopoverPrimitive.Positioner
data-slot="popover-content"
sideOffset={sideOffset}
className={cn("z-50", className)}
{...props}
>
<PopoverPrimitive.Popup
className={cn(
"w-72 rounded-xl border bg-popover p-4 text-popover-foreground shadow-md outline-none",
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
"data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
)}
>
{props.children}
</PopoverPrimitive.Popup>
</PopoverPrimitive.Positioner>
</PopoverPortal>
)
}
function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
return <PopoverPrimitive.Close data-slot="popover-close" {...props} />
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="popover-header" className={cn("flex flex-col gap-1.5", className)} {...props} />
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h4">) {
return <h4 data-slot="popover-title" className={cn("text-sm font-semibold leading-none", className)} {...props} />
}
function PopoverDescription({ className, ...props }: React.ComponentProps<"p">) {
return <p data-slot="popover-description" className={cn("text-sm text-muted-foreground", className)} {...props} />
}
export { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverPortal, PopoverTitle, PopoverTrigger }
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: progress_bar
kind: component
lang: typescript
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."
tags: [progress, loading, component, ui, feedback]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+60
View File
@@ -0,0 +1,60 @@
---
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. Base-UI RadioGroup + Radio primitives."
tags: [radio, radio-group, component, ui, interactive, form, base-ui]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react/radio-group", "@base-ui/react/radio"]
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
<RadioGroup defaultValue="option-a">
<RadioGroupItem value="option-a" label="Opcion A" />
<RadioGroupItem value="option-b" label="Opcion B" />
<RadioGroupItem value="option-c" label="Opcion C" disabled />
</RadioGroup>
```
## Notas
RadioGroup es el contenedor (Base-UI RadioGroup). RadioGroupItem es cada opcion individual (Base-UI Radio). El id de cada item se genera con useId si no se provee.
+61
View File
@@ -0,0 +1,61 @@
import * as React from "react"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
import { Radio } from "@base-ui/react/radio"
import { cn } from "../core/cn"
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
return (
<RadioGroupPrimitive
data-slot="radio-group"
className={cn("grid gap-2", className)}
{...props}
/>
)
}
interface RadioGroupItemProps extends Radio.Root.Props {
label?: string
className?: string
labelClassName?: string
}
function RadioGroupItem({ className, label, id, labelClassName, ...props }: RadioGroupItemProps) {
const internalId = React.useId()
const itemId = id ?? internalId
return (
<div className="flex items-center gap-2">
<Radio.Root
id={itemId}
data-slot="radio-group-item"
className={cn(
"aspect-square size-4 rounded-full border border-input bg-transparent transition-colors outline-none",
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"data-checked:border-primary",
"disabled:pointer-events-none disabled:opacity-50",
className
)}
{...props}
>
<Radio.Indicator
data-slot="radio-group-indicator"
className="flex items-center justify-center"
>
<span className="block size-2 rounded-full bg-primary" />
</Radio.Indicator>
</Radio.Root>
{label && (
<label
htmlFor={itemId}
data-slot="radio-group-label"
className={cn("text-sm font-medium leading-none cursor-pointer select-none", labelClassName)}
>
{label}
</label>
)}
</div>
)
}
export { RadioGroup, RadioGroupItem }
export type { RadioGroupItemProps }
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: select
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Select<T>(props: SelectRootProps<T>): JSX.Element"
description: "Select genérico accesible con grupos, separadores y animaciones. Base-UI primitive con posicionamiento automático."
tags: [select, form, dropdown, component, ui, interactive]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: settings_page
kind: function
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: pure
signature: "settingsPage(props: SettingsPageProps): ReactElement"
description: "Genera una página de configuración con navegación lateral, secciones y campos de formulario (text, number, toggle, select, textarea)."
tags: [settings, page, form, sections, factory, composition, ui]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+71
View File
@@ -0,0 +1,71 @@
---
name: sheet
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Sheet(props: SheetProps): JSX.Element"
description: "Panel lateral deslizante (drawer) accesible con variantes de lado y animaciones. Base-UI Dialog con posicionamiento lateral via CVA."
tags: [sheet, drawer, panel, component, ui, interactive, overlay, base-ui, cva]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react/dialog", "class-variance-authority", "lucide-react"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/sheet.tsx"
props:
- name: side
type: "'top' | 'bottom' | 'left' | 'right'"
required: false
description: "Lado desde el que aparece el panel (default: right)"
- name: showCloseButton
type: "boolean"
required: false
description: "Muestra el boton de cierre (default: true)"
- name: open
type: "boolean"
required: false
description: "Estado controlado de apertura"
- name: onOpenChange
type: "(open: boolean) => void"
required: false
description: "Callback cuando cambia el estado"
emits: [onOpenChange]
has_state: false
framework: react
variant: [top, bottom, left, right]
---
## Ejemplo
```tsx
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Abrir panel</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Editar perfil</SheetTitle>
<SheetDescription>Realiza cambios en tu perfil.</SheetDescription>
</SheetHeader>
<div className="py-4">
{/* contenido del panel */}
</div>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Cancelar</Button>
</SheetClose>
<Button>Guardar</Button>
</SheetFooter>
</SheetContent>
</Sheet>
```
## Notas
Reutiliza Base-UI Dialog para el comportamiento modal. Las animaciones de deslizamiento usan slide-in-from-* de Tailwind. CVA gestiona las variantes de lado. Exports: Sheet, SheetTrigger, SheetContent, SheetClose, SheetPortal, SheetOverlay, SheetHeader, SheetFooter, SheetTitle, SheetDescription.
+118
View File
@@ -0,0 +1,118 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { XIcon } from "lucide-react"
import { cn } from "../core/cn"
function Sheet({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/50",
"data-open:animate-in data-open:fade-in-0",
"data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
const sheetVariants = cva(
"fixed z-50 flex flex-col gap-4 bg-background p-6 shadow-lg transition ease-in-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-open:animate-in data-open:slide-in-from-top data-closed:animate-out data-closed:slide-out-to-top",
bottom: "inset-x-0 bottom-0 border-t data-open:animate-in data-open:slide-in-from-bottom data-closed:animate-out data-closed:slide-out-to-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-open:animate-in data-open:slide-in-from-left data-closed:animate-out data-closed:slide-out-to-left sm:max-w-sm",
right: "inset-y-0 right-0 h-full w-3/4 border-l data-open:animate-in data-open:slide-in-from-right data-closed:animate-out data-closed:slide-out-to-right sm:max-w-sm",
},
},
defaultVariants: { side: "right" },
}
)
interface SheetContentProps
extends DialogPrimitive.Popup.Props,
VariantProps<typeof sheetVariants> {
showCloseButton?: boolean
}
function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }: SheetContentProps) {
return (
<SheetPortal>
<SheetOverlay />
<DialogPrimitive.Popup
data-slot="sheet-content"
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="sheet-close-button"
className="absolute top-4 right-4 inline-flex size-7 items-center justify-center rounded-md opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5", className)} {...props} />
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...props}
/>
)
}
function SheetTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="sheet-title"
className={cn("text-base font-semibold leading-none", className)}
{...props}
/>
)
}
function SheetDescription({ className, ...props }: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, sheetVariants }
export type { SheetContentProps }
+82
View File
@@ -0,0 +1,82 @@
---
name: simple_select
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "SimpleSelect(props: SimpleSelectProps): JSX.Element"
description: "Select simplificado que acepta un array plano o agrupado de opciones. Wrapper sobre Select con API declarativa."
tags: [select, dropdown, form, component, ui, simple]
uses_functions: [cn_ts_core, select_ts_ui]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/simple_select.tsx"
props:
- name: value
type: "string"
required: true
description: "Valor seleccionado actualmente"
- name: onValueChange
type: "(value: string) => void"
required: true
description: "Callback cuando cambia la seleccion"
- name: options
type: "SimpleSelectOption[] | SimpleSelectGroup[]"
required: true
description: "Opciones planas o agrupadas"
- name: placeholder
type: "string"
required: false
description: "Texto cuando no hay seleccion"
- name: disabled
type: "boolean"
required: false
description: "Deshabilita el select"
- name: size
type: "'sm' | 'default'"
required: false
description: "Tamano del trigger"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: [default, sm]
---
## Ejemplo
```tsx
import { SimpleSelect } from '@fn_library'
// Opciones planas
const options = [
{ value: 'a', label: 'Opcion A' },
{ value: 'b', label: 'Opcion B' },
]
<SimpleSelect value={selected} onValueChange={setSelected} options={options} />
// Opciones agrupadas
const grouped = [
{ group: 'Frutas', items: [{ value: 'apple', label: 'Manzana' }] },
{ group: 'Verduras', items: [{ value: 'carrot', label: 'Zanahoria' }] },
]
<SimpleSelect value={selected} onValueChange={setSelected} options={grouped} />
```
## Notas
- Detecta automaticamente si las opciones son planas o agrupadas via type guard `isGrouped`.
- Wrapper sobre `Select` del registry — toda la logica de Base-UI y accesibilidad viene del componente base.
- Soporta items deshabilitados individualmente con `disabled: true` en cada opcion.
+84
View File
@@ -0,0 +1,84 @@
"use client"
import * as React from "react"
import { cn } from "../core/cn"
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
SelectGroup,
SelectGroupLabel,
} from "./select"
export interface SimpleSelectOption {
value: string
label: string
disabled?: boolean
}
export interface SimpleSelectGroup {
group: string
items: SimpleSelectOption[]
}
export type SimpleSelectOptions = SimpleSelectOption[] | SimpleSelectGroup[]
interface SimpleSelectProps {
value: string
onValueChange: (value: string) => void
options: SimpleSelectOptions
placeholder?: string
disabled?: boolean
size?: 'sm' | 'default'
className?: string
}
function isGrouped(options: SimpleSelectOptions): options is SimpleSelectGroup[] {
return options.length > 0 && 'group' in options[0]
}
function SimpleSelect({
value,
onValueChange,
options,
placeholder = "Select...",
disabled = false,
size = 'default',
className,
}: SimpleSelectProps) {
return (
<Select value={value} onValueChange={onValueChange} disabled={disabled}>
<SelectTrigger
className={cn(
size === 'sm' && 'h-7 text-xs px-2',
className
)}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{isGrouped(options)
? options.map(g => (
<SelectGroup key={g.group}>
<SelectGroupLabel>{g.group}</SelectGroupLabel>
{g.items.map(item => (
<SelectItem key={item.value} value={item.value} disabled={item.disabled}>
{item.label}
</SelectItem>
))}
</SelectGroup>
))
: (options as SimpleSelectOption[]).map(item => (
<SelectItem key={item.value} value={item.value} disabled={item.disabled}>
{item.label}
</SelectItem>
))
}
</SelectContent>
</Select>
)
}
export { SimpleSelect }
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: skeleton
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Skeleton(props: HTMLAttributes<HTMLDivElement>): JSX.Element"
description: "Sistema de loading skeletons: base, text, card, avatar, button, table. Variantes preconfiguradas para estados de carga."
tags: [skeleton, loading, placeholder, component, ui]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: sparkline
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Sparkline(props: SparklineProps): JSX.Element"
description: "Mini gráfico inline SVG puro (sin Recharts) con variantes line, area y bar. Para KPI cards y tablas."
tags: [sparkline, chart, inline, svg, component, ui, visualization]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+67
View File
@@ -0,0 +1,67 @@
---
name: switch_toggle
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "SwitchToggle(props: SwitchToggleProps): JSX.Element"
description: "Toggle on/off accesible con label opcional a izquierda o derecha. Base-UI Switch primitive."
tags: [switch, toggle, component, ui, interactive, form, base-ui]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react/switch"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/switch_toggle.tsx"
props:
- name: label
type: "string"
required: false
description: "Texto de etiqueta visible junto al switch"
- name: labelPosition
type: "'left' | 'right'"
required: false
description: "Posicion del label respecto al switch (default: right)"
- name: checked
type: "boolean"
required: false
description: "Estado controlado del toggle"
- name: defaultChecked
type: "boolean"
required: false
description: "Estado inicial no controlado"
- name: disabled
type: "boolean"
required: false
description: "Deshabilita el toggle"
- name: onCheckedChange
type: "(checked: boolean) => void"
required: false
description: "Callback cuando cambia el estado"
emits: [onCheckedChange]
has_state: false
framework: react
variant: []
---
## Ejemplo
```tsx
// Label a la derecha (default)
<SwitchToggle label="Notificaciones" defaultChecked />
// Label a la izquierda
<SwitchToggle label="Modo oscuro" labelPosition="left" checked={dark} onCheckedChange={setDark} />
// Solo switch sin label
<SwitchToggle checked={enabled} onCheckedChange={setEnabled} />
```
## Notas
Usa Base-UI Switch primitive. El thumb se traslada con translate-x via Tailwind. El id se genera con useId si no se provee para conectar el label.
+66
View File
@@ -0,0 +1,66 @@
import * as React from "react"
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
import { cn } from "../core/cn"
interface SwitchToggleProps extends SwitchPrimitive.Root.Props {
label?: string
labelPosition?: "left" | "right"
className?: string
}
function SwitchToggle({ className, label, labelPosition = "right", id, ...props }: SwitchToggleProps) {
const internalId = React.useId()
const switchId = id ?? internalId
const switchEl = (
<SwitchPrimitive.Root
id={switchId}
data-slot="switch"
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors outline-none",
"bg-input focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"data-checked:bg-primary",
"disabled:pointer-events-none disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"pointer-events-none block size-4 rounded-full bg-background shadow-sm ring-0 transition-transform",
"translate-x-0 data-checked:translate-x-4"
)}
/>
</SwitchPrimitive.Root>
)
if (!label) return switchEl
return (
<div className="flex items-center gap-2">
{labelPosition === "left" && (
<label
htmlFor={switchId}
data-slot="switch-label"
className="text-sm font-medium leading-none cursor-pointer select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
>
{label}
</label>
)}
{switchEl}
{labelPosition === "right" && (
<label
htmlFor={switchId}
data-slot="switch-label"
className="text-sm font-medium leading-none cursor-pointer select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
>
{label}
</label>
)}
</div>
)
}
export { SwitchToggle }
export type { SwitchToggleProps }
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: tabs
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Tabs(props: TabsRootProps): JSX.Element"
description: "Sistema de tabs con orientación horizontal/vertical, variantes default y line, y soporte para iconos. Base-UI primitive."
tags: [tabs, navigation, component, ui, interactive]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+66
View File
@@ -0,0 +1,66 @@
---
name: textarea
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Textarea(props: TextareaProps): JSX.Element"
description: "Input multilinea accesible con auto-resize opcional. Patron identico a Input para consistencia de estilos."
tags: [textarea, component, ui, interactive, form]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/textarea.tsx"
props:
- name: autoResize
type: "boolean"
required: false
description: "Ajusta la altura automaticamente al contenido (default: false)"
- name: placeholder
type: "string"
required: false
description: "Texto placeholder"
- name: disabled
type: "boolean"
required: false
description: "Deshabilita el textarea"
- name: rows
type: "number"
required: false
description: "Numero de filas visibles iniciales"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: [onChange, onFocus, onBlur]
has_state: true
framework: react
variant: []
---
## Ejemplo
```tsx
// Basico
<Textarea placeholder="Escribe aqui..." rows={4} />
// Con auto-resize
<Textarea autoResize placeholder="Crece automaticamente..." />
// Controlado
<Textarea value={text} onChange={(e) => setText(e.target.value)} />
// Con validacion
<Textarea aria-invalid={!!error} />
```
## Notas
Usa forwardRef para compatibilidad con form libraries. El auto-resize ajusta style.height en cada cambio — por eso requiere has_state: true. Aplica las mismas clases de foco y validacion que Input para consistencia visual.
+47
View File
@@ -0,0 +1,47 @@
import * as React from "react"
import { cn } from "../core/cn"
interface TextareaProps extends React.ComponentPropsWithoutRef<"textarea"> {
autoResize?: boolean
}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, autoResize = false, onChange, ...props }, ref) => {
const internalRef = React.useRef<HTMLTextAreaElement>(null)
const resolvedRef = (ref as React.RefObject<HTMLTextAreaElement>) ?? internalRef
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (autoResize && resolvedRef.current) {
resolvedRef.current.style.height = "auto"
resolvedRef.current.style.height = `${resolvedRef.current.scrollHeight}px`
}
onChange?.(e)
},
[autoResize, onChange, resolvedRef]
)
return (
<textarea
ref={resolvedRef}
data-slot="textarea"
className={cn(
"min-h-[80px] w-full rounded-lg border border-input bg-transparent px-3 py-2 text-sm transition-colors outline-none",
"placeholder:text-muted-foreground",
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50",
"aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20",
"dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
autoResize && "resize-none overflow-hidden",
className
)}
onChange={handleChange}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }
export type { TextareaProps }
+3 -3
View File
@@ -1,15 +1,15 @@
---
name: theme_provider
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "ThemeProvider(props: { children: ReactNode; themes: Record<string, Theme>; defaultTheme?: string }): JSX.Element"
description: "Provider de tema React con context, persistencia en localStorage, detección de preferencia del sistema y hook useTheme."
tags: [theme, provider, context, hook, component, ui]
uses_functions: [apply_theme_typescript_ui]
uses_types: [ThemeConfig_typescript_ui]
uses_functions: [apply_theme_ts_ui]
uses_types: [ThemeConfig_ts_ui]
returns: []
returns_optional: false
error_type: ""
+90
View File
@@ -0,0 +1,90 @@
---
name: toast
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Toast(props: ToastProps): JSX.Element"
description: "Notificaciones temporales con variantes semanticas (success, error, warning, info), iconos automaticos, auto-dismiss y provider con hook useToast."
tags: [toast, notification, alert, component, ui, interactive, cva]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["class-variance-authority", "lucide-react"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/toast.tsx"
props:
- name: variant
type: "'default' | 'success' | 'error' | 'warning' | 'info'"
required: false
description: "Variante semantica con icono automatico (default: default)"
- name: title
type: "string"
required: false
description: "Titulo de la notificacion"
- name: description
type: "string"
required: false
description: "Texto descriptivo secundario"
- name: action
type: "React.ReactNode"
required: false
description: "Accion opcional (boton, link) debajo del contenido"
- name: onClose
type: "() => void"
required: false
description: "Callback al cerrar. Muestra el boton X si se provee."
- name: duration
type: "number"
required: false
description: "Duracion en ms antes del auto-dismiss (default: 5000, 0 = persistente)"
emits: [onClose]
has_state: true
framework: react
variant: [default, success, error, warning, info]
---
## Ejemplo
```tsx
// 1. Envolver la app con el provider
<ToastProvider position="bottom-right">
<App />
</ToastProvider>
// 2. Usar el hook en cualquier componente
function MyComponent() {
const { toast } = useToast()
return (
<Button onClick={() => toast({
variant: "success",
title: "Guardado",
description: "Los cambios se guardaron correctamente.",
})}>
Guardar
</Button>
)
}
// Toast con accion
toast({
variant: "error",
title: "Error al guardar",
description: "Intenta de nuevo.",
action: <Button size="sm" variant="outline">Reintentar</Button>,
duration: 0, // persistente hasta cerrar manualmente
})
// Toast individual sin provider
<Toast variant="info" title="Informacion" description="Texto descriptivo" onClose={() => {}} />
```
## Notas
Arquitectura: Toast (componente visual puro), ToastViewport (contenedor posicionado fixed), ToastProvider (context + logica de estado), useToast (hook consumidor). Los iconos son automaticos segun variante: CheckCircle2 (success), AlertCircle (error), AlertTriangle (warning), Info (info). El border-l-4 diferencia visualmente cada variante. ToastProvider acepta position con 6 posiciones predefinidas.
+170
View File
@@ -0,0 +1,170 @@
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { XIcon, CheckCircle2Icon, AlertCircleIcon, AlertTriangleIcon, InfoIcon } from "lucide-react"
import { cn } from "../core/cn"
const toastVariants = cva(
"group/toast pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-xl border p-4 pr-8 shadow-lg transition-all",
{
variants: {
variant: {
default: "bg-background text-foreground border-border",
success: "bg-background border-l-4 border-l-green-500 border-border text-foreground",
error: "bg-background border-l-4 border-l-destructive border-border text-foreground",
warning: "bg-background border-l-4 border-l-yellow-500 border-border text-foreground",
info: "bg-background border-l-4 border-l-blue-500 border-border text-foreground",
},
},
defaultVariants: { variant: "default" },
}
)
const variantIcons: Record<string, React.ReactNode> = {
success: <CheckCircle2Icon className="mt-0.5 size-4 shrink-0 text-green-500" />,
error: <AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />,
warning: <AlertTriangleIcon className="mt-0.5 size-4 shrink-0 text-yellow-500" />,
info: <InfoIcon className="mt-0.5 size-4 shrink-0 text-blue-500" />,
}
interface ToastProps
extends React.ComponentPropsWithoutRef<"div">,
VariantProps<typeof toastVariants> {
title?: string
description?: string
action?: React.ReactNode
onClose?: () => void
}
const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
({ className, variant = "default", title, description, action, onClose, children, ...props }, ref) => {
return (
<div
ref={ref}
data-slot="toast"
data-variant={variant}
role="alert"
aria-live="polite"
className={cn(toastVariants({ variant }), className)}
{...props}
>
{variant && variant !== "default" && variantIcons[variant]}
<div className="flex flex-1 flex-col gap-1">
{title && (
<div data-slot="toast-title" className="text-sm font-semibold leading-none">
{title}
</div>
)}
{description && (
<div data-slot="toast-description" className="text-sm text-muted-foreground">
{description}
</div>
)}
{children}
{action && <div data-slot="toast-action" className="mt-2">{action}</div>}
</div>
{onClose && (
<button
data-slot="toast-close"
onClick={onClose}
className="absolute top-2 right-2 inline-flex size-5 items-center justify-center rounded-md text-muted-foreground opacity-70 transition-opacity hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
aria-label="Close"
>
<XIcon className="size-3.5" />
</button>
)}
</div>
)
}
)
Toast.displayName = "Toast"
interface ToastViewportProps extends React.ComponentPropsWithoutRef<"div"> {
position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"
}
const positionClasses: Record<NonNullable<ToastViewportProps["position"]>, string> = {
"top-left": "top-4 left-4",
"top-center": "top-4 left-1/2 -translate-x-1/2",
"top-right": "top-4 right-4",
"bottom-left": "bottom-4 left-4",
"bottom-center": "bottom-4 left-1/2 -translate-x-1/2",
"bottom-right": "bottom-4 right-4",
}
function ToastViewport({ className, position = "bottom-right", ...props }: ToastViewportProps) {
return (
<div
data-slot="toast-viewport"
className={cn(
"fixed z-[100] flex max-h-screen w-full max-w-sm flex-col gap-2 p-4",
positionClasses[position],
className
)}
{...props}
/>
)
}
type ToastEntry = ToastProps & {
id: string
duration?: number
}
interface ToastProviderContextValue {
toasts: ToastEntry[]
toast: (props: Omit<ToastEntry, "id">) => string
dismiss: (id: string) => void
dismissAll: () => void
}
const ToastContext = React.createContext<ToastProviderContextValue | null>(null)
function ToastProvider({ children, position = "bottom-right" }: { children: React.ReactNode; position?: ToastViewportProps["position"] }) {
const [toasts, setToasts] = React.useState<ToastEntry[]>([])
const dismiss = React.useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id))
}, [])
const dismissAll = React.useCallback(() => {
setToasts([])
}, [])
const toast = React.useCallback(
(props: Omit<ToastEntry, "id">) => {
const id = Math.random().toString(36).slice(2)
const duration = props.duration ?? 5000
setToasts((prev) => [...prev, { ...props, id }])
if (duration > 0) {
setTimeout(() => dismiss(id), duration)
}
return id
},
[dismiss]
)
return (
<ToastContext.Provider value={{ toasts, toast, dismiss, dismissAll }}>
{children}
<ToastViewport position={position}>
{toasts.map((t) => {
const { id, duration: _duration, ...rest } = t
return (
<Toast key={id} {...rest} onClose={() => dismiss(id)} />
)
})}
</ToastViewport>
</ToastContext.Provider>
)
}
function useToast() {
const ctx = React.useContext(ToastContext)
if (!ctx) throw new Error("useToast must be used within ToastProvider")
return ctx
}
export { Toast, ToastProvider, ToastViewport, toastVariants, useToast }
export type { ToastEntry, ToastProps, ToastViewportProps }
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: tooltip
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Tooltip(props: TooltipRootProps): JSX.Element"
description: "Tooltip accesible con animaciones, posicionamiento automático y arrow. Base-UI primitive con delay configurable."
tags: [tooltip, overlay, component, ui, help]
uses_functions: [cn_typescript_core]
uses_functions: [cn_ts_core]
uses_types: []
returns: []
returns_optional: false
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: use_animated_canvas
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: use_wails_event
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
+3 -3
View File
@@ -1,15 +1,15 @@
---
name: use_wails_mutation
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "useWailsMutation<TData, TVariables>(opts: UseWailsMutationOptions<TData, TVariables>): UseWailsMutationResult<TData, TVariables>"
description: "Hook para escrituras IPC Wails con optimistic updates, invalidación automática de queries, retry y callbacks completos."
tags: [wails, mutation, hook, ipc, optimistic, component, ui]
uses_functions: [wails_cache_typescript_core, wails_provider_typescript_ui]
uses_types: [WailsIPC_typescript_ui]
uses_functions: [wails_cache_ts_core, wails_provider_ts_ui]
uses_types: [WailsIPC_ts_ui]
returns: []
returns_optional: false
error_type: ""
+3 -3
View File
@@ -1,15 +1,15 @@
---
name: use_wails_query
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "useWailsQuery<T>(opts: UseWailsQueryOptions<T>): UseWailsQueryResult<T>"
description: "Hook React Query-like sobre IPC Wails. Cache automático, refetch por intervalo/foco, retry con backoff, invalidación."
tags: [wails, query, hook, ipc, cache, component, ui]
uses_functions: [wails_cache_typescript_core, wails_provider_typescript_ui]
uses_types: [WailsIPC_typescript_ui]
uses_functions: [wails_cache_ts_core, wails_provider_ts_ui]
uses_types: [WailsIPC_ts_ui]
returns: []
returns_optional: false
error_type: ""
+2 -2
View File
@@ -1,14 +1,14 @@
---
name: use_wails_stream
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "useWailsStream<T>(opts: UseWailsStreamOptions<T>): UseWailsStreamResult<T>"
description: "Hook para streaming de datos Go→TS con buffer configurable, auto-complete, transform y control start/stop. Incluye useWailsLogs."
tags: [wails, stream, hook, ipc, realtime, buffer, component, ui]
uses_functions: [use_wails_event_typescript_ui]
uses_functions: [use_wails_event_ts_ui]
uses_types: []
returns: []
returns_optional: false
+3 -3
View File
@@ -1,15 +1,15 @@
---
name: wails_provider
kind: component
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "WailsProvider(props: { children: ReactNode; cache?: WailsCache; defaultQueryOptions?: QueryOptions }): JSX.Element"
description: "Provider React para IPC Wails con cache context, opciones default y fallback a singleton. Exporta useWailsContext y useWailsCache."
tags: [wails, provider, context, ipc, component, ui]
uses_functions: [wails_cache_typescript_core]
uses_types: [WailsIPC_typescript_ui]
uses_functions: [wails_cache_ts_core]
uses_types: [WailsIPC_ts_ui]
returns: []
returns_optional: false
error_type: ""
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: ComponentVariants
lang: typescript
lang: ts
domain: core
version: "1.0.0"
algebraic: product
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: ChartSeries
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
algebraic: product
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: ThemeConfig
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
algebraic: product
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: WailsIPC
lang: typescript
lang: ts
domain: ui
version: "1.0.0"
algebraic: product
+1 -1
View File
@@ -17,7 +17,7 @@ imports: [fmt, strconv, strings]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/cdp_click.go"
file_path: "functions/browser/cdp_click.go"
---
## Ejemplo
+1 -1
View File
@@ -17,7 +17,7 @@ imports: [fmt, os]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/cdp_close.go"
file_path: "functions/browser/cdp_close.go"
---
## Ejemplo
+2 -2
View File
@@ -16,8 +16,8 @@ error_type: "error_go_core"
imports: [fmt, net, net/url, strings]
tested: true
tests: ["TestChromeLaunchAndConnect"]
test_file_path: "functions/infra/chrome_launch_test.go"
file_path: "functions/infra/cdp_connect.go"
test_file_path: "functions/browser/chrome_launch_test.go"
file_path: "functions/browser/cdp_connect.go"
---
## Ejemplo
+2 -2
View File
@@ -16,8 +16,8 @@ error_type: "error_go_core"
imports: [fmt]
tested: true
tests: ["TestCdpEvaluate"]
test_file_path: "functions/infra/chrome_launch_test.go"
file_path: "functions/infra/cdp_evaluate.go"
test_file_path: "functions/browser/chrome_launch_test.go"
file_path: "functions/browser/cdp_evaluate.go"
---
## Ejemplo
+2 -2
View File
@@ -16,8 +16,8 @@ error_type: "error_go_core"
imports: [fmt]
tested: true
tests: ["TestCdpGetHTML"]
test_file_path: "functions/infra/chrome_launch_test.go"
file_path: "functions/infra/cdp_get_html.go"
test_file_path: "functions/browser/chrome_launch_test.go"
file_path: "functions/browser/cdp_get_html.go"
---
## Ejemplo
+2 -2
View File
@@ -16,8 +16,8 @@ error_type: "error_go_core"
imports: [fmt]
tested: true
tests: ["TestChromeLaunchAndConnect"]
test_file_path: "functions/infra/chrome_launch_test.go"
file_path: "functions/infra/cdp_navigate.go"
test_file_path: "functions/browser/chrome_launch_test.go"
file_path: "functions/browser/cdp_navigate.go"
---
## Ejemplo
+2 -2
View File
@@ -16,8 +16,8 @@ error_type: "error_go_core"
imports: [encoding/base64, fmt, os, path/filepath]
tested: true
tests: ["TestCdpScreenshot"]
test_file_path: "functions/infra/chrome_launch_test.go"
file_path: "functions/infra/cdp_screenshot.go"
test_file_path: "functions/browser/chrome_launch_test.go"
file_path: "functions/browser/cdp_screenshot.go"
---
## Ejemplo
+1 -1
View File
@@ -17,7 +17,7 @@ imports: [fmt, time]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/cdp_type_text.go"
file_path: "functions/browser/cdp_type_text.go"
---
## Ejemplo
+1 -1
View File
@@ -17,7 +17,7 @@ imports: [fmt, time]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/cdp_wait_element.go"
file_path: "functions/browser/cdp_wait_element.go"
---
## Ejemplo
+1 -1
View File
@@ -17,7 +17,7 @@ imports: [fmt, time]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/cdp_wait_load.go"
file_path: "functions/browser/cdp_wait_load.go"
---
## Ejemplo
+2 -2
View File
@@ -16,8 +16,8 @@ error_type: "error_go_core"
imports: [fmt, net, os, os/exec, time]
tested: true
tests: ["TestFindChrome", "TestChromeLaunchAndConnect"]
test_file_path: "functions/infra/chrome_launch_test.go"
file_path: "functions/infra/chrome_launch.go"
test_file_path: "functions/browser/chrome_launch_test.go"
file_path: "functions/browser/chrome_launch.go"
---
## Ejemplo
@@ -0,0 +1,10 @@
package cybersecurity
// OsintCryptoWallet represents a cryptocurrency wallet tracked in an OSINT investigation.
type OsintCryptoWallet struct {
Address string `json:"address"`
Blockchain string `json:"blockchain"`
Balance float64 `json:"balance"`
FirstSeen string `json:"first_seen"`
LastSeen string `json:"last_seen"`
}

Some files were not shown because too many files have changed in this diff Show More