merge: quick/frontmatter-fixes-new-components-osint — frontmatter fixes, UI components, OSINT types, indexer warnings
This commit is contained in:
@@ -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
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
@@ -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,7 +1,7 @@
|
||||
---
|
||||
name: chart_colors
|
||||
kind: function
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: cn
|
||||
kind: function
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
|
||||
@@ -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 <style> 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,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,7 +1,7 @@
|
||||
---
|
||||
name: wails_cache
|
||||
kind: function
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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 }} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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.
|
||||
@@ -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 }
|
||||
@@ -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,7 +1,7 @@
|
||||
---
|
||||
name: use_animated_canvas
|
||||
kind: component
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: use_wails_event
|
||||
kind: component
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
---
|
||||
name: ComponentVariants
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
algebraic: product
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: ChartSeries
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
algebraic: product
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: ThemeConfig
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
algebraic: product
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: WailsIPC
|
||||
lang: typescript
|
||||
lang: ts
|
||||
domain: ui
|
||||
version: "1.0.0"
|
||||
algebraic: product
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user