feat: funciones frontend React/TS — componentes UI, hooks Wails, charts y tipos

Componentes React reutilizables: card, dialog, tabs, select, alert, badge, button, input, label,
skeleton, tooltip, progress_bar, page_header, form_field, settings_page, crud_page, analytics_page,
dashboard_layout. Charts: area, bar, line, sparkline, kpi_card, chart_container.
Hooks Wails: use_wails_query, use_wails_mutation, use_wails_stream, use_wails_event, use_animated_canvas.
Funciones core: cn, format_compact, chart_colors, get_series_color, wails_cache, theme_config_to_colors.
Tipos: chart_series, wails_ipc, theme_config, component_variants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:55:34 +02:00
parent 6deef76427
commit 953f598b9b
86 changed files with 5721 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
---
name: chart_colors
kind: function
lang: typescript
domain: core
version: "1.0.0"
purity: pure
signature: "getChartColor(index: number): string"
description: "Paleta de colores para gráficos basada en CSS variables del tema activo. Colores accesibles por índice cíclico."
tags: [chart, color, theme, palette, visualization]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/chart_colors.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/charts/chart-base.tsx"
---
## Ejemplo
```typescript
getChartColor(0) // 'hsl(var(--chart-1, 220 70% 50%))'
getChartColor(7) // 'hsl(var(--chart-3, 30 80% 55%))' — cicla sobre 5 colores
```
## Notas
Usa CSS variables del tema con fallback hardcodeado. Los colores cambian automáticamente con el tema activo. También exporta `chartColors` (array) para uso directo.
+11
View File
@@ -0,0 +1,11 @@
export const chartColors = [
'hsl(var(--chart-1, 220 70% 50%))',
'hsl(var(--chart-2, 160 60% 45%))',
'hsl(var(--chart-3, 30 80% 55%))',
'hsl(var(--chart-4, 280 65% 60%))',
'hsl(var(--chart-5, 340 75% 55%))',
]
export function getChartColor(index: number): string {
return chartColors[index % chartColors.length]
}
+36
View File
@@ -0,0 +1,36 @@
---
name: cn
kind: function
lang: typescript
domain: core
version: "1.0.0"
purity: pure
signature: "cn(...inputs: ClassValue[]): string"
description: "Combina clases CSS con clsx y resuelve conflictos Tailwind con tailwind-merge. Utilidad fundamental para composición de estilos."
tags: [css, tailwind, classname, merge, utility]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [clsx, tailwind-merge]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/cn.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/utils.ts"
---
## Ejemplo
```typescript
cn("px-4 py-2", "px-6") // "px-6 py-2" (tailwind-merge resuelve conflicto)
cn("text-red-500", false && "hidden") // "text-red-500" (clsx filtra falsy)
cn("rounded-lg", className) // composición con className externo
```
## Notas
Base de todo el sistema de estilos. Todos los componentes la usan para componer className.
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs))
}
+36
View File
@@ -0,0 +1,36 @@
---
name: format_compact
kind: function
lang: typescript
domain: core
version: "1.0.0"
purity: pure
signature: "formatCompact(n: number, decimals?: number): string"
description: "Familia de funciones de formato compacto: números (K/M/B), frecuencia (Hz/KHz/MHz), bytes (KB/MB/GB), duración (ms/s/min/h)."
tags: [format, number, compact, utility, display]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/format_compact.ts"
---
## Ejemplo
```typescript
formatCompact(1234) // '1.2K'
formatCompact(1500000) // '1.5M'
formatHz(44100) // '44.1 KHz'
formatBytes(1073741824) // '1.0 GB'
formatDuration(3500) // '3.5s'
formatDuration(0.5) // '500µs'
```
## Notas
Todas son funciones puras sin dependencias. Útiles en dashboards, KPI cards, tablas y tooltips.
+42
View File
@@ -0,0 +1,42 @@
/**
* Formatea un número en formato compacto (1K, 1.2M, etc.)
* Soporta sufijos personalizados.
*/
export function formatCompact(n: number, decimals: number = 1): string {
if (Math.abs(n) >= 1_000_000_000) return (n / 1_000_000_000).toFixed(decimals) + 'B'
if (Math.abs(n) >= 1_000_000) return (n / 1_000_000).toFixed(decimals) + 'M'
if (Math.abs(n) >= 1_000) return (n / 1_000).toFixed(decimals) + 'K'
return n.toString()
}
/**
* Formatea frecuencia en Hz/KHz/MHz/GHz.
*/
export function formatHz(hz: number, decimals: number = 1): string {
if (hz >= 1_000_000_000) return (hz / 1_000_000_000).toFixed(decimals) + ' GHz'
if (hz >= 1_000_000) return (hz / 1_000_000).toFixed(decimals) + ' MHz'
if (hz >= 1_000) return (hz / 1_000).toFixed(decimals) + ' KHz'
return hz + ' Hz'
}
/**
* Formatea bytes en KB/MB/GB/TB.
*/
export function formatBytes(bytes: number, decimals: number = 1): string {
if (bytes >= 1_099_511_627_776) return (bytes / 1_099_511_627_776).toFixed(decimals) + ' TB'
if (bytes >= 1_073_741_824) return (bytes / 1_073_741_824).toFixed(decimals) + ' GB'
if (bytes >= 1_048_576) return (bytes / 1_048_576).toFixed(decimals) + ' MB'
if (bytes >= 1_024) return (bytes / 1_024).toFixed(decimals) + ' KB'
return bytes + ' B'
}
/**
* Formatea duración en ms/s/min/h.
*/
export function formatDuration(ms: number): string {
if (ms >= 3_600_000) return (ms / 3_600_000).toFixed(1) + 'h'
if (ms >= 60_000) return (ms / 60_000).toFixed(1) + 'min'
if (ms >= 1_000) return (ms / 1_000).toFixed(1) + 's'
if (ms >= 1) return ms.toFixed(0) + 'ms'
return (ms * 1000).toFixed(0) + 'µs'
}
@@ -0,0 +1,36 @@
---
name: get_series_color
kind: function
lang: typescript
domain: core
version: "1.0.0"
purity: pure
signature: "getSeriesColor(index: number, color?: string): string"
description: "Devuelve color para una serie de gráfico por índice cíclico, o el color explícito si se proporciona."
tags: [chart, color, series, visualization]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/get_series_color.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/charts/chart-base.tsx"
---
## Ejemplo
```typescript
getSeriesColor(0) // '#3b82f6'
getSeriesColor(5) // '#3b82f6' (cicla sobre 5 colores)
getSeriesColor(0, '#ff0000') // '#ff0000' (usa el explícito)
```
## Notas
Paleta fija de 5 colores: azul, verde, ámbar, violeta, rosa. También exporta `defaultColors` para uso directo.
@@ -0,0 +1,7 @@
const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']
export function getSeriesColor(index: number, color?: string): string {
return color || defaultColors[index % defaultColors.length]
}
export { defaultColors }
@@ -0,0 +1,37 @@
---
name: theme_config_to_colors
kind: function
lang: typescript
domain: core
version: "1.0.0"
purity: pure
signature: "themeConfigToColors(config: ThemeConfig): ThemeColors"
description: "Convierte un ThemeConfig completo a ThemeColors plano para inyectar como CSS variables. Mapea tokens semánticos a variables CSS."
tags: [theme, colors, css-variables, conversion]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/theme_config_to_colors.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/themes/types.ts"
---
## Ejemplo
```typescript
const colors = themeConfigToColors(darkThemeConfig)
// { background: '...', foreground: '...', primary: '...', ... }
```
## Notas
Puente entre el sistema de temas estructurado (ThemeConfig) y el sistema plano de CSS variables que consumen los componentes.
Depende de los tipos ThemeConfig y ThemeColors definidos en `frontend/types/ui/theme_config.ts`. El tipo aún no está indexado en la BD (pendiente añadir theme_config.md para que fn index lo registre).
@@ -0,0 +1,49 @@
import type { ThemeConfig, ThemeColors } from "../../types/ui/theme_config"
export function themeConfigToColors(config: ThemeConfig): ThemeColors {
const { colors } = config
return {
background: colors.background.default,
foreground: colors.foreground.default,
card: colors.surface.raised,
cardForeground: colors.foreground.default,
popover: colors.surface.overlay,
popoverForeground: colors.foreground.default,
primary: colors.brand.primary,
primaryForeground: colors.brand.primaryForeground,
secondary: colors.brand.secondary,
secondaryForeground: colors.brand.secondaryForeground,
muted: colors.background.muted,
mutedForeground: colors.foreground.muted,
accent: colors.brand.accent,
accentForeground: colors.brand.accentForeground,
destructive: colors.status.error,
destructiveForeground: colors.status.errorForeground,
success: colors.status.success,
successForeground: colors.status.successForeground,
warning: colors.status.warning,
warningForeground: colors.status.warningForeground,
info: colors.status.info,
infoForeground: colors.status.infoForeground,
surface: colors.surface.raised,
surfaceHover: colors.background.subtle,
overlay: colors.surface.overlay,
border: colors.border.default,
input: colors.border.default,
ring: colors.ring,
chart1: colors.chart[1],
chart2: colors.chart[2],
chart3: colors.chart[3],
chart4: colors.chart[4],
chart5: colors.chart[5],
sidebar: colors.sidebar.background,
sidebarForeground: colors.sidebar.foreground,
sidebarPrimary: colors.brand.primary,
sidebarPrimaryForeground: colors.brand.primaryForeground,
sidebarAccent: colors.sidebar.accent,
sidebarAccentForeground: colors.sidebar.accentForeground,
sidebarBorder: colors.sidebar.border,
sidebarRing: colors.sidebar.ring,
}
}
+39
View File
@@ -0,0 +1,39 @@
---
name: wails_cache
kind: function
lang: typescript
domain: core
version: "1.0.0"
purity: pure
signature: "class WailsCache { get<T>(key: string[]): T | null; set<T>(key: string[], data: T): void; invalidate(key: string[]): void; subscribe(key: string[], cb: () => void): () => void }"
description: "Cache reactivo para IPC Wails con invalidación por prefijo, suscripción a cambios y tracking de staleness. Singleton global."
tags: [wails, cache, ipc, reactive, state]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/wails_cache.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/wails/cache.ts"
---
## Ejemplo
```typescript
import { wailsCache } from './wails_cache'
wailsCache.set(['users', '123'], userData)
const user = wailsCache.get<User>(['users', '123'])
wailsCache.invalidate(['users']) // invalida users:*
const unsub = wailsCache.subscribe(['users'], () => console.log('changed'))
```
## Notas
Key como string[] permite invalidación jerárquica: `invalidate(['users'])` invalida `users`, `users:123`, `users:456`, etc.
+99
View File
@@ -0,0 +1,99 @@
interface CacheEntry {
data: unknown
timestamp: Date
}
export class WailsCache {
private cache = new Map<string, CacheEntry>()
private subscribers = new Map<string, Set<() => void>>()
/** Generar key string desde array */
private getKey(queryKey: string[]): string {
return queryKey.join(':')
}
/** Obtener dato del cache */
get<T>(queryKey: string[]): T | null {
const entry = this.cache.get(this.getKey(queryKey))
return (entry?.data as T) ?? null
}
/** Guardar dato en cache */
set<T>(queryKey: string[], data: T): void {
const key = this.getKey(queryKey)
this.cache.set(key, { data, timestamp: new Date() })
this.notifySubscribers(key)
}
/** Verificar si existe en cache */
has(queryKey: string[]): boolean {
return this.cache.has(this.getKey(queryKey))
}
/** Obtener timestamp de última actualización */
getTimestamp(queryKey: string[]): Date | null {
const entry = this.cache.get(this.getKey(queryKey))
return entry?.timestamp ?? null
}
/** Verificar si los datos están stale */
isStale(queryKey: string[], staleTime: number): boolean {
const entry = this.cache.get(this.getKey(queryKey))
if (!entry) return true
return Date.now() - entry.timestamp.getTime() > staleTime
}
/** Invalidar cache (esta key y todas las que empiezan igual) */
invalidate(queryKey: string[]): void {
const prefix = this.getKey(queryKey)
const keysToDelete: string[] = []
for (const key of this.cache.keys()) {
if (key === prefix || key.startsWith(prefix + ':')) {
keysToDelete.push(key)
}
}
keysToDelete.forEach((key) => {
this.cache.delete(key)
this.notifySubscribers(key)
})
}
/** Limpiar todo el cache */
clear(): void {
this.cache.clear()
this.subscribers.forEach((_, key) => this.notifySubscribers(key))
}
/** Subscribirse a cambios en una key */
subscribe(queryKey: string[], callback: () => void): () => void {
const key = this.getKey(queryKey)
if (!this.subscribers.has(key)) {
this.subscribers.set(key, new Set())
}
this.subscribers.get(key)!.add(callback)
return () => {
this.subscribers.get(key)?.delete(callback)
}
}
/** Notificar a subscribers */
private notifySubscribers(key: string): void {
this.subscribers.get(key)?.forEach((callback) => callback())
}
/** Obtener tamaño del cache */
get size(): number {
return this.cache.size
}
/** Obtener todas las keys */
keys(): string[] {
return Array.from(this.cache.keys())
}
}
// Singleton global
export const wailsCache = new WailsCache()
+49
View File
@@ -0,0 +1,49 @@
---
name: alert
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react, class-variance-authority]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/alert.tsx"
props:
- name: variant
type: "'default' | 'destructive'"
required: false
description: "Variante visual"
emits: []
has_state: false
framework: react
variant: [default, destructive]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/alert.tsx"
---
## Ejemplo
```tsx
<Alert variant="destructive">
<AlertCircle />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong.</AlertDescription>
</Alert>
```
## Notas
Exporta 4 subcomponentes composables via data-slot: Alert, AlertTitle, AlertDescription, AlertAction.
El icono SVG se posiciona automáticamente en grid cuando es hijo directo de Alert.
AlertAction se posiciona absolute top-right para acciones secundarias (ej: botón cerrar).
+34
View File
@@ -0,0 +1,34 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive: "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
},
},
defaultVariants: { variant: "default" },
}
)
function Alert({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="alert-title" className={cn("font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", className)} {...props} />
}
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="alert-description" className={cn("text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4", className)} {...props} />
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="alert-action" className={cn("absolute top-2 right-2", className)} {...props} />
}
export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants }
+47
View File
@@ -0,0 +1,47 @@
---
name: analytics_page
kind: function
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/analytics_page.tsx"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: ""
---
## Ejemplo
```tsx
analyticsPage({
title: 'Sales Analytics',
metrics: [
{ label: 'Revenue', value: '$124,500', delta: { value: 12.5, isPositive: true } },
{ label: 'Orders', value: '1,234', delta: { value: -3.2, isPositive: false } },
{ label: 'Avg Order', value: '$101', delta: { value: 0, isPositive: true } },
{ label: 'Customers', value: '892' },
],
charts: [
{ id: 'revenue', title: 'Revenue Over Time', type: 'area', span: 2, content: <AreaChart data={revenueData} xKey="month" yKey="revenue" /> },
{ id: 'orders', title: 'Orders by Category', type: 'bar', content: <BarChart data={orderData} xKey="category" yKey="count" /> },
{ id: 'trends', title: 'Customer Trends', type: 'line', content: <LineChart data={trendData} xKey="week" yKey="customers" /> },
],
})
```
## Notas
Layout inteligente: los KPIs se ajustan automáticamente a 2/3/4 columnas según cantidad. Los charts soportan span para ancho completo.
+101
View File
@@ -0,0 +1,101 @@
import * as React from 'react'
import { cn } from '../core/cn'
interface MetricConfig {
label: string
value: string | number
delta?: { value: number; isPositive: boolean }
sparklineData?: number[]
}
interface ChartConfig {
id: string
title: string
type: 'line' | 'bar' | 'area'
span?: 1 | 2
height?: number
content: React.ReactNode
}
interface AnalyticsPageProps {
title: string
subtitle?: string
dateRange?: React.ReactNode
metrics: MetricConfig[]
charts: ChartConfig[]
actions?: React.ReactNode
className?: string
}
export function analyticsPage({
title,
subtitle,
dateRange,
metrics,
charts,
actions,
className,
}: AnalyticsPageProps): React.ReactElement {
return (
<div className={cn('space-y-6', className)}>
{/* Header */}
<div className="flex items-center justify-between border-b pb-4">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
</div>
<div className="flex items-center gap-2">
{dateRange}
{actions}
</div>
</div>
{/* KPI Row */}
<div className={cn(
'grid gap-4',
metrics.length <= 2 ? 'grid-cols-1 md:grid-cols-2' :
metrics.length <= 3 ? 'grid-cols-1 md:grid-cols-3' :
'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
)}>
{metrics.map((metric, i) => (
<div key={i} className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
<p className="text-sm text-muted-foreground">{metric.label}</p>
<div className="mt-2 flex items-end justify-between gap-4">
<div className="space-y-1">
<p className="text-3xl font-bold tracking-tight">{metric.value}</p>
{metric.delta && (
<div className={cn(
'flex items-center gap-1 text-sm font-medium',
metric.delta.value === 0 ? 'text-muted-foreground' :
metric.delta.isPositive ? 'text-green-600 dark:text-green-500' :
'text-red-600 dark:text-red-500'
)}>
<span>{metric.delta.value > 0 ? '+' : ''}{metric.delta.value}%</span>
</div>
)}
</div>
</div>
</div>
))}
</div>
{/* Charts Grid */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{charts.map((chart) => (
<div
key={chart.id}
className={cn(
'rounded-xl border bg-card p-4 text-card-foreground shadow-sm',
chart.span === 2 && 'lg:col-span-2'
)}
>
<h3 className="mb-3 text-sm font-medium text-muted-foreground">{chart.title}</h3>
{chart.content}
</div>
))}
</div>
</div>
)
}
export type { AnalyticsPageProps, MetricConfig, ChartConfig }
+40
View File
@@ -0,0 +1,40 @@
---
name: apply_theme
kind: function
lang: typescript
domain: ui
version: "1.0.0"
purity: impure
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]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/apply_theme.tsx"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/hooks/use-theme.tsx"
---
## Ejemplo
```typescript
import { applyTheme } from './apply_theme'
applyTheme({
name: 'dark',
label: 'Oscuro',
colors: themeConfigToColors(darkThemeConfig)
})
```
## Notas
Función impura (modifica el DOM). Mapea cada key de ThemeColors a una CSS variable. Temas oscuros (dark, midnight, sunset) añaden clase `dark` al root.
+111
View File
@@ -0,0 +1,111 @@
interface ThemeColors {
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
warning: string
warningForeground: string
info: string
infoForeground: string
surface: string
surfaceHover: string
overlay: string
border: string
input: string
ring: string
chart1: string
chart2: string
chart3: string
chart4: string
chart5: string
sidebar: string
sidebarForeground: string
sidebarPrimary: string
sidebarPrimaryForeground: string
sidebarAccent: string
sidebarAccentForeground: string
sidebarBorder: string
sidebarRing: string
}
interface Theme {
name: string
label: string
colors: ThemeColors
}
const cssVarMap: Record<keyof ThemeColors, string> = {
background: '--background',
foreground: '--foreground',
card: '--card',
cardForeground: '--card-foreground',
popover: '--popover',
popoverForeground: '--popover-foreground',
primary: '--primary',
primaryForeground: '--primary-foreground',
secondary: '--secondary',
secondaryForeground: '--secondary-foreground',
muted: '--muted',
mutedForeground: '--muted-foreground',
accent: '--accent',
accentForeground: '--accent-foreground',
destructive: '--destructive',
destructiveForeground: '--destructive-foreground',
success: '--success',
successForeground: '--success-foreground',
warning: '--warning',
warningForeground: '--warning-foreground',
info: '--info',
infoForeground: '--info-foreground',
surface: '--surface',
surfaceHover: '--surface-hover',
overlay: '--overlay',
border: '--border',
input: '--input',
ring: '--ring',
chart1: '--chart-1',
chart2: '--chart-2',
chart3: '--chart-3',
chart4: '--chart-4',
chart5: '--chart-5',
sidebar: '--sidebar',
sidebarForeground: '--sidebar-foreground',
sidebarPrimary: '--sidebar-primary',
sidebarPrimaryForeground: '--sidebar-primary-foreground',
sidebarAccent: '--sidebar-accent',
sidebarAccentForeground: '--sidebar-accent-foreground',
sidebarBorder: '--sidebar-border',
sidebarRing: '--sidebar-ring',
}
export function applyTheme(theme: Theme): void {
const root = document.documentElement
const colors = theme.colors
Object.entries(cssVarMap).forEach(([key, cssVar]) => {
const value = colors[key as keyof ThemeColors]
root.style.setProperty(cssVar, value)
})
if (theme.name === 'dark' || theme.name === 'midnight' || theme.name === 'sunset') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}
export type { Theme, ThemeColors }
+56
View File
@@ -0,0 +1,56 @@
---
name: area_chart
kind: component
lang: typescript
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]
returns: []
returns_optional: false
error_type: ""
imports: [recharts]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/area_chart.tsx"
props:
- name: data
type: "Record<string, unknown>[]"
required: true
description: "Array de datos"
- name: xKey
type: "string"
required: true
description: "Key del eje X"
- name: stacked
type: "boolean"
required: false
description: "Apilar áreas"
- name: gradient
type: "GradientConfig | boolean"
required: false
description: "Gradiente (true por defecto)"
- name: series
type: "Series[]"
required: false
description: "Series de datos para multi-series"
emits: []
has_state: false
framework: react
variant: [default, stacked]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/charts/area-chart.tsx"
---
## Ejemplo
```tsx
<AreaChart data={data} xKey="date" yKey="revenue" gradient />
<AreaChart data={data} xKey="date" series={series} stacked showLegend />
```
+62
View File
@@ -0,0 +1,62 @@
import {
AreaChart as RechartsAreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
} from 'recharts'
import { ChartContainer, ChartTooltipContent, type Series, getSeriesColor } from './chart_container'
interface GradientConfig { from: string; to: string }
interface AreaChartProps {
data: Record<string, unknown>[]
xKey: string
yKey?: string
series?: Series[]
stacked?: boolean
gradient?: GradientConfig | boolean
showGrid?: boolean
showLegend?: boolean
height?: number | string
className?: string
xAxisFormatter?: (value: unknown) => string
yAxisFormatter?: (value: unknown) => string
valueFormatter?: (value: number) => string
}
function AreaChartComponent({
data, xKey, yKey, series, stacked = false, gradient = true, showGrid = true,
showLegend = false, height = 300, className, xAxisFormatter, yAxisFormatter,
valueFormatter = (v) => v.toLocaleString(),
}: AreaChartProps) {
const areas = series
? series.map((s, i) => ({ dataKey: s.key, name: s.name, color: getSeriesColor(i, s.color) }))
: yKey ? [{ dataKey: yKey, name: yKey, color: getSeriesColor(0) }] : []
const gradientConfig: GradientConfig | null = gradient
? typeof gradient === 'object' ? gradient : { from: '', to: 'transparent' }
: null
return (
<ChartContainer className={className} height={height}>
<RechartsAreaChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
<defs>
{areas.map((area) => (
<linearGradient key={area.dataKey} id={`gradient-${area.dataKey}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={gradientConfig?.from || area.color} stopOpacity={0.8} />
<stop offset="95%" stopColor={gradientConfig?.to || area.color} stopOpacity={0.1} />
</linearGradient>
))}
</defs>
{showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />}
<XAxis dataKey={xKey} tickFormatter={xAxisFormatter} className="text-xs fill-muted-foreground" />
<YAxis tickFormatter={yAxisFormatter} className="text-xs fill-muted-foreground" />
<Tooltip content={<ChartTooltipContent valueFormatter={valueFormatter} />} cursor={{ stroke: 'hsl(var(--muted-foreground))', strokeDasharray: '3 3' }} />
{showLegend && <Legend />}
{areas.map((area) => (
<Area key={area.dataKey} type="monotone" dataKey={area.dataKey} name={area.name} stroke={area.color} strokeWidth={2} fill={gradient ? `url(#gradient-${area.dataKey})` : area.color} fillOpacity={gradient ? 1 : 0.3} stackId={stacked ? 'stack' : undefined} />
))}
</RechartsAreaChart>
</ChartContainer>
)
}
export const AreaChart = AreaChartComponent
export type { AreaChartProps, GradientConfig }
+48
View File
@@ -0,0 +1,48 @@
---
name: badge
kind: component
lang: ts
domain: ui
version: "1.0.0"
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["class-variance-authority"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/badge.tsx"
props:
- name: variant
type: "'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'success' | 'warning' | 'error' | 'info'"
required: false
description: "Variante visual"
- name: size
type: "'default' | 'sm'"
required: false
description: "Tamaño"
emits: []
has_state: false
framework: react
variant: [default, secondary, destructive, outline, ghost, link, success, warning, error, info]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/badge.tsx"
---
## Ejemplo
```tsx
<Badge variant="success">Active</Badge>
<Badge variant="error" size="sm">Error</Badge>
```
## Notas
Versión simplificada que usa span nativo en lugar de useRender de Base-UI. Mantiene todas las variantes y la composibilidad con cn().
+45
View File
@@ -0,0 +1,45 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive: "bg-destructive/10 text-destructive [a]:hover:bg-destructive/20",
outline: "border-border text-foreground [a]:hover:bg-muted",
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline",
success: "bg-green-500/10 text-green-600 dark:bg-green-500/20 dark:text-green-400",
warning: "bg-yellow-500/10 text-yellow-600 dark:bg-yellow-500/20 dark:text-yellow-400",
error: "bg-red-500/10 text-red-600 dark:bg-red-500/20 dark:text-red-400",
info: "bg-blue-500/10 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400",
},
size: {
default: "h-5 px-2 text-xs",
sm: "h-4 px-1.5 text-[10px]",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant = "default", size = "default", ...props }: BadgeProps) {
return (
<span
data-slot="badge"
className={cn(badgeVariants({ variant, size }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
+52
View File
@@ -0,0 +1,52 @@
---
name: bar_chart
kind: component
lang: typescript
domain: ui
version: "1.0.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]
returns: []
returns_optional: false
error_type: ""
imports: [recharts]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/bar_chart.tsx"
props:
- name: data
type: "Record<string, unknown>[]"
required: true
description: "Array de datos"
- name: xKey
type: "string"
required: true
description: "Key del eje X/categoría"
- name: horizontal
type: "boolean"
required: false
description: "Orientación horizontal"
- name: series
type: "Series[]"
required: false
description: "Series de datos para multi-series"
emits: []
has_state: false
framework: react
variant: [vertical, horizontal]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/charts/bar-chart.tsx"
---
## Ejemplo
```tsx
<BarChart data={data} xKey="category" yKey="sales" showLegend />
<BarChart data={data} xKey="name" series={series} horizontal />
```
+53
View File
@@ -0,0 +1,53 @@
import {
BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
} from 'recharts'
import { ChartContainer, ChartTooltipContent, type Series, getSeriesColor } from './chart_container'
interface BarChartProps {
data: Record<string, unknown>[]
xKey: string
yKey?: string
series?: Series[]
horizontal?: boolean
showGrid?: boolean
showLegend?: boolean
height?: number | string
className?: string
xAxisFormatter?: (value: unknown) => string
yAxisFormatter?: (value: unknown) => string
valueFormatter?: (value: number) => string
}
function BarChartComponent({
data, xKey, yKey, series, horizontal = false, showGrid = true, showLegend = false,
height = 300, className, xAxisFormatter, yAxisFormatter, valueFormatter = (v) => v.toLocaleString(),
}: BarChartProps) {
const bars = series
? series.map((s, i) => ({ dataKey: s.key, name: s.name, fill: getSeriesColor(i, s.color) }))
: yKey ? [{ dataKey: yKey, name: yKey, fill: getSeriesColor(0) }] : []
return (
<ChartContainer className={className} height={height}>
<RechartsBarChart data={data} layout={horizontal ? 'vertical' : 'horizontal'} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
{showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />}
{horizontal ? (
<>
<XAxis type="number" tickFormatter={yAxisFormatter} className="text-xs fill-muted-foreground" />
<YAxis dataKey={xKey} type="category" tickFormatter={xAxisFormatter} width={80} className="text-xs fill-muted-foreground" />
</>
) : (
<>
<XAxis dataKey={xKey} tickFormatter={xAxisFormatter} className="text-xs fill-muted-foreground" />
<YAxis tickFormatter={yAxisFormatter} className="text-xs fill-muted-foreground" />
</>
)}
<Tooltip content={<ChartTooltipContent valueFormatter={valueFormatter} />} cursor={{ fill: 'hsl(var(--muted) / 0.3)' }} />
{showLegend && <Legend />}
{bars.map((bar) => <Bar key={bar.dataKey} dataKey={bar.dataKey} name={bar.name} fill={bar.fill} radius={[4, 4, 0, 0]} />)}
</RechartsBarChart>
</ChartContainer>
)
}
export const BarChart = BarChartComponent
export type { BarChartProps }
+53
View File
@@ -0,0 +1,53 @@
---
name: button
kind: component
lang: ts
domain: ui
version: "1.0.0"
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_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/button.tsx"
props:
- name: variant
type: "'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link'"
required: false
description: "Estilo visual del botón"
- name: size
type: "'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'"
required: false
description: "Tamaño del botón"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: [onClick]
has_state: false
framework: react
variant: [default, outline, secondary, ghost, destructive, link]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/button.tsx"
---
## Ejemplo
```tsx
<Button variant="outline" size="sm">Click me</Button>
<Button variant="destructive">Delete</Button>
<Button variant="ghost" size="icon"><TrashIcon /></Button>
```
## Notas
Componente base del sistema. Usa Base-UI Button primitive para accesibilidad completa (keyboard, ARIA). CVA para gestión type-safe de variantes.
+52
View File
@@ -0,0 +1,52 @@
"use client"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem]",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)]",
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)]",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }
+54
View File
@@ -0,0 +1,54 @@
---
name: card
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Card(props: { size?: 'default' | 'sm'; className?: string; children: ReactNode }): JSX.Element"
description: "Contenedor card con header, title, description, action, content y footer. Sistema de slots composable."
tags: [card, container, layout, component, ui]
uses_functions: [cn_typescript_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["react"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/card.tsx"
props:
- name: size
type: "'default' | 'sm'"
required: false
description: "Tamaño del card"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: [default, sm]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/card.tsx"
---
## Ejemplo
```tsx
<Card>
<CardHeader>
<CardTitle>Título</CardTitle>
<CardDescription>Descripción</CardDescription>
</CardHeader>
<CardContent>Contenido</CardContent>
<CardFooter>Footer</CardFooter>
</Card>
```
## Notas
Sistema de slots via data-slot attributes. Card detecta automáticamente la presencia de CardFooter y ajusta el padding. Exporta 7 subcomponentes composables.
+85
View File
@@ -0,0 +1,85 @@
import * as React from "react"
import { cn } from "../core/cn"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3", className)}
{...props}
/>
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }
+49
View File
@@ -0,0 +1,49 @@
---
name: chart_container
kind: component
lang: typescript
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]
returns: []
returns_optional: false
error_type: ""
imports: [recharts, react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/chart_container.tsx"
props:
- name: height
type: "number | string"
required: false
description: "Altura del chart (default 300)"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/charts/chart-base.tsx"
---
## Ejemplo
```tsx
<ChartContainer height={400}>
<RechartsLineChart data={data}>...</RechartsLineChart>
</ChartContainer>
```
## Notas
Exporta: ChartContainer, ChartTooltipContent, ChartTooltip, ChartLegend, chartColors, defaultColors, getSeriesColor, Series.
+80
View File
@@ -0,0 +1,80 @@
import * as React from 'react'
import { cn } from '../core/cn'
import { ResponsiveContainer, Tooltip as RechartsTooltip, Legend as RechartsLegend } from 'recharts'
export const chartColors = [
'hsl(var(--chart-1, 220 70% 50%))',
'hsl(var(--chart-2, 160 60% 45%))',
'hsl(var(--chart-3, 30 80% 55%))',
'hsl(var(--chart-4, 280 65% 60%))',
'hsl(var(--chart-5, 340 75% 55%))',
]
export const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']
export interface Series {
key: string
name: string
color?: string
}
export function getSeriesColor(index: number, color?: string): string {
return color || defaultColors[index % defaultColors.length]
}
interface ChartContainerProps {
children: React.ReactNode
className?: string
height?: number | string
}
export function ChartContainer({ children, className, height = 300 }: ChartContainerProps) {
return (
<div
className={cn('w-full', className)}
style={{ height, minWidth: 100, minHeight: typeof height === 'number' ? Math.min(height, 100) : 100 }}
>
<ResponsiveContainer width="100%" height="100%">
{children as React.ReactElement}
</ResponsiveContainer>
</div>
)
}
interface ChartTooltipContentProps {
active?: boolean
payload?: Array<{ name: string; value: number; color: string; dataKey: string }>
label?: string
labelFormatter?: (label: string) => string
valueFormatter?: (value: number) => string
}
export function ChartTooltipContent({
active, payload, label,
labelFormatter = (l) => l,
valueFormatter = (v) => v.toLocaleString(),
}: ChartTooltipContentProps) {
if (!active || !payload?.length) return null
return (
<div className="rounded-lg border bg-background p-2 shadow-md">
<p className="mb-1 text-sm font-medium">{labelFormatter(label || '')}</p>
<div className="space-y-0.5">
{payload.map((entry, index) => (
<div key={index} className="flex items-center gap-2 text-sm">
<div className="size-2.5 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-muted-foreground">{entry.name}:</span>
<span className="font-medium">{valueFormatter(entry.value)}</span>
</div>
))}
</div>
</div>
)
}
export function ChartTooltip(props: React.ComponentProps<typeof RechartsTooltip>) {
return <RechartsTooltip content={<ChartTooltipContent />} cursor={{ fill: 'hsl(var(--muted) / 0.3)' }} {...props} />
}
export function ChartLegend(props: React.ComponentProps<typeof RechartsLegend>) {
return <RechartsLegend wrapperStyle={{ paddingTop: 16 }} {...props} />
}
+51
View File
@@ -0,0 +1,51 @@
---
name: crud_page
kind: function
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/crud_page.tsx"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: ""
---
## Ejemplo
```tsx
crudPage({
title: 'Users',
subtitle: 'Manage system users',
data: users,
fields: [
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'email', label: 'Email', type: 'email', required: true },
{ key: 'role', label: 'Role', type: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }] },
],
columns: [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'role', label: 'Role', render: (v) => <Badge variant={v === 'admin' ? 'default' : 'secondary'}>{v}</Badge> },
],
onAdd: handleAdd,
onEdit: handleEdit,
onDelete: handleDelete,
})
```
## Notas
El schema de campos se almacena como data attribute para que un agente pueda leerlo y generar el formulario de diálogo correspondiente. La tabla incluye sorting visual implícito por columnas.
+120
View File
@@ -0,0 +1,120 @@
import * as React from 'react'
import { cn } from '../core/cn'
interface CrudField {
key: string
label: string
type: 'text' | 'number' | 'email' | 'select' | 'textarea'
required?: boolean
options?: Array<{ label: string; value: string }>
placeholder?: string
}
interface CrudPageProps<T extends Record<string, unknown>> {
title: string
subtitle?: string
data: T[]
fields: CrudField[]
columns: Array<{
key: keyof T
label: string
render?: (value: unknown, row: T) => React.ReactNode
}>
onAdd?: (item: Partial<T>) => void
onEdit?: (item: T) => void
onDelete?: (item: T) => void
actions?: React.ReactNode
className?: string
}
export function crudPage<T extends Record<string, unknown>>({
title,
subtitle,
data,
fields,
columns,
onAdd,
onEdit,
onDelete,
actions,
className,
}: CrudPageProps<T>): React.ReactElement {
return (
<div className={cn('space-y-6', className)}>
{/* Header */}
<div className="flex items-center justify-between border-b pb-4">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
</div>
<div className="flex items-center gap-2">
{actions}
{onAdd && (
<button className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/80">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 5v14M5 12h14"/></svg>
Add {title.replace(/s$/, '')}
</button>
)}
</div>
</div>
{/* Table */}
<div className="rounded-lg border">
<table className="w-full caption-bottom text-sm">
<thead className="border-b bg-muted/50">
<tr>
{columns.map((col) => (
<th key={String(col.key)} className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">
{col.label}
</th>
))}
{(onEdit || onDelete) && (
<th className="px-4 py-3 text-right text-sm font-medium text-muted-foreground">Actions</th>
)}
</tr>
</thead>
<tbody className="divide-y">
{data.length === 0 ? (
<tr>
<td colSpan={columns.length + (onEdit || onDelete ? 1 : 0)} className="h-24 text-center text-muted-foreground">
No items yet.
</td>
</tr>
) : (
data.map((row, i) => (
<tr key={i} className="hover:bg-muted/50">
{columns.map((col) => (
<td key={String(col.key)} className="px-4 py-3 align-middle">
{col.render ? col.render(row[col.key], row) : String(row[col.key] ?? '')}
</td>
))}
{(onEdit || onDelete) && (
<td className="px-4 py-3 text-right">
<div className="flex justify-end gap-1">
{onEdit && (
<button onClick={() => onEdit(row)} className="inline-flex size-7 items-center justify-center rounded-md hover:bg-muted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
)}
{onDelete && (
<button onClick={() => onDelete(row)} className="inline-flex size-7 items-center justify-center rounded-md text-destructive hover:bg-destructive/10">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
)}
</div>
</td>
)}
</tr>
))
)}
</tbody>
</table>
</div>
{/* Form fields definition (for agent use — renders a form preview) */}
<div className="hidden" data-slot="crud-form-schema" data-fields={JSON.stringify(fields)} />
</div>
)
}
export type { CrudPageProps, CrudField }
+42
View File
@@ -0,0 +1,42 @@
---
name: dashboard_layout
kind: function
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/dashboard_layout.tsx"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: ""
---
## Ejemplo
```tsx
dashboardLayout({
columns: 4,
widgets: [
{ id: 'revenue', title: 'Revenue', content: <KPICard label="Revenue" value="$12k" /> },
{ id: 'users', title: 'Users', content: <KPICard label="Users" value={1234} /> },
{ id: 'chart', title: 'Trends', span: 2, content: <LineChart data={data} xKey="month" yKey="value" /> },
{ id: 'table', span: 4, content: <DataTable columns={cols} data={rows} /> },
]
})
```
## Notas
Factory pura — dado el mismo input siempre genera el mismo JSX. Un agente puede construir dashboards completos pasando widgets como configuración declarativa.
@@ -0,0 +1,67 @@
import * as React from 'react'
import { cn } from '../core/cn'
interface DashboardWidget {
id: string
title?: string
span?: 1 | 2 | 3 | 4
rowSpan?: 1 | 2
content: React.ReactNode
}
interface DashboardLayoutProps {
widgets: DashboardWidget[]
columns?: 1 | 2 | 3 | 4
gap?: 'sm' | 'md' | 'lg'
className?: string
}
const gapClasses = { sm: 'gap-2', md: 'gap-4', lg: 'gap-6' }
const spanClasses: Record<number, string> = {
1: 'col-span-1',
2: 'col-span-1 md:col-span-2',
3: 'col-span-1 md:col-span-2 lg:col-span-3',
4: 'col-span-1 md:col-span-2 lg:col-span-4',
}
const rowSpanClasses: Record<number, string> = {
1: 'row-span-1',
2: 'row-span-2',
}
export function dashboardLayout({
widgets,
columns = 4,
gap = 'md',
className,
}: DashboardLayoutProps): React.ReactElement {
const gridCols: Record<number, string> = {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
}
return (
<div className={cn('grid', gridCols[columns], gapClasses[gap], className)}>
{widgets.map((widget) => (
<div
key={widget.id}
className={cn(
'rounded-xl border bg-card p-4 text-card-foreground shadow-sm',
spanClasses[widget.span || 1],
rowSpanClasses[widget.rowSpan || 1]
)}
>
{widget.title && (
<h3 className="mb-3 text-sm font-medium text-muted-foreground">{widget.title}</h3>
)}
{widget.content}
</div>
))}
</div>
)
}
export type { DashboardWidget, DashboardLayoutProps }
+54
View File
@@ -0,0 +1,54 @@
---
name: detail_page
kind: function
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/detail_page.tsx"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: ""
---
## Ejemplo
```tsx
detailPage({
title: 'John Doe',
subtitle: 'john@example.com',
badge: <Badge variant="success">Active</Badge>,
onBack: () => router.back(),
fields: [
{ label: 'Role', value: 'Administrator' },
{ label: 'Created', value: 'Mar 15, 2026' },
{ label: 'Bio', value: 'Full stack developer...', span: 2 },
],
tabs: [
{ label: 'Projects', value: 'projects', count: 12, content: <ProjectList /> },
{ label: 'Activity', value: 'activity', count: 48, content: <ActivityList /> },
],
activeTab: 'projects',
timeline: [
{ id: '1', title: 'Deployed v2.1', timestamp: '2 hours ago', variant: 'success' },
{ id: '2', title: 'Updated settings', timestamp: 'Yesterday' },
{ id: '3', title: 'Created project', timestamp: 'Mar 10, 2026' },
],
})
```
## Notas
Factory completa para páginas de detalle. Combina header con back/avatar/badge, grid de metadata, tabs con badges de conteo, y timeline de actividad con variantes de color semántico.
+134
View File
@@ -0,0 +1,134 @@
import * as React from 'react'
import { cn } from '../core/cn'
interface DetailField {
label: string
value: React.ReactNode
span?: 1 | 2
}
interface DetailTab {
label: string
value: string
content: React.ReactNode
count?: number
}
interface TimelineEvent {
id: string
title: string
description?: string
timestamp: string
icon?: React.ReactNode
variant?: 'default' | 'success' | 'warning' | 'error'
}
interface DetailPageProps {
title: string
subtitle?: string
badge?: React.ReactNode
avatar?: React.ReactNode
actions?: React.ReactNode
onBack?: () => void
fields: DetailField[]
tabs?: DetailTab[]
activeTab?: string
onTabChange?: (value: string) => void
timeline?: TimelineEvent[]
className?: string
}
const variantDotColors = {
default: 'bg-primary',
success: 'bg-green-500',
warning: 'bg-amber-500',
error: 'bg-red-500',
}
export function detailPage({
title, subtitle, badge, avatar, actions, onBack,
fields, tabs, activeTab, onTabChange, timeline, className,
}: DetailPageProps): React.ReactElement {
return (
<div className={cn('space-y-6', className)}>
{/* Header */}
<div className="flex items-start justify-between border-b pb-4">
<div className="flex items-start gap-4">
{onBack && (
<button onClick={onBack} className="mt-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-muted">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m15 18-6-6 6-6"/></svg>
</button>
)}
{avatar && <div className="size-12 shrink-0 overflow-hidden rounded-full bg-muted">{avatar}</div>}
<div className="space-y-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{badge}
</div>
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
</div>
</div>
{actions && <div className="flex items-center gap-2">{actions}</div>}
</div>
{/* Fields grid */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{fields.map((field, i) => (
<div key={i} className={cn('space-y-1', field.span === 2 && 'md:col-span-2')}>
<p className="text-sm text-muted-foreground">{field.label}</p>
<div className="text-sm font-medium">{field.value}</div>
</div>
))}
</div>
{/* Tabs */}
{tabs && tabs.length > 0 && (
<div className="space-y-4">
<nav className="flex gap-4 border-b">
{tabs.map((tab) => (
<button
key={tab.value}
type="button"
onClick={() => onTabChange?.(tab.value)}
className={cn(
'inline-flex items-center gap-2 border-b-2 px-1 pb-3 text-sm font-medium transition-colors',
activeTab === tab.value ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'
)}
>
{tab.label}
{tab.count !== undefined && (
<span className="inline-flex h-5 items-center rounded-full bg-muted px-2 text-xs font-medium">{tab.count}</span>
)}
</button>
))}
</nav>
{tabs.find(t => t.value === activeTab)?.content}
</div>
)}
{/* Timeline */}
{timeline && timeline.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-muted-foreground">Activity</h3>
<div className="space-y-0">
{timeline.map((event, i) => (
<div key={event.id} className="flex gap-3 pb-4">
<div className="flex flex-col items-center">
<div className={cn('mt-1 size-2.5 rounded-full', variantDotColors[event.variant || 'default'])} />
{i < timeline.length - 1 && <div className="flex-1 w-px bg-border" />}
</div>
<div className="flex-1 space-y-0.5 pb-2">
<p className="text-sm font-medium">{event.title}</p>
{event.description && <p className="text-xs text-muted-foreground">{event.description}</p>}
<p className="text-xs text-muted-foreground/70">{event.timestamp}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
export type { DetailPageProps, DetailField, DetailTab, TimelineEvent }
+55
View File
@@ -0,0 +1,55 @@
---
name: dialog
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", lucide-react, react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/dialog.tsx"
props:
- name: showCloseButton
type: "boolean"
required: false
description: "Mostrar botón de cerrar (default true)"
emits: [onOpenChange]
has_state: true
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/dialog.tsx"
---
## Ejemplo
```tsx
<Dialog>
<DialogTrigger asChild><Button>Open</Button></DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Título</DialogTitle>
<DialogDescription>Descripción</DialogDescription>
</DialogHeader>
<p>Contenido</p>
<DialogFooter>
<Button>Confirmar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
```
## Notas
10 subcomponentes exportados. Base-UI Dialog primitive para accesibilidad completa (focus trap, escape, click outside).
+73
View File
@@ -0,0 +1,73 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "../core/cn"
import { XIcon } from "lucide-react"
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn("fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0", className)}
{...props}
/>
)
}
function DialogContent({ className, children, showCloseButton = true, ...props }: DialogPrimitive.Popup.Props & { showCloseButton?: boolean }) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn("fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-sm ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm 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}
{showCloseButton && (
<DialogPrimitive.Close data-slot="dialog-close" className="absolute top-2 right-2 inline-flex size-7 items-center justify-center rounded-md hover:bg-muted">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="dialog-header" className={cn("flex flex-col gap-2", className)} {...props} />
}
function DialogFooter({ className, children, ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="dialog-footer" className={cn("-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end", className)} {...props}>
{children}
</div>
)
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return <DialogPrimitive.Title data-slot="dialog-title" className={cn("text-base leading-none font-medium", className)} {...props} />
}
function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
return <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-sm text-muted-foreground", className)} {...props} />
}
export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger }
+53
View File
@@ -0,0 +1,53 @@
---
name: form_field
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/form_field.tsx"
props:
- name: label
type: "string"
required: false
description: "Texto del label"
- name: helperText
type: "string"
required: false
description: "Texto de ayuda"
- name: error
type: "string"
required: false
description: "Mensaje de error (reemplaza helperText)"
- name: children
type: "ReactNode"
required: true
description: "Input o componente de formulario"
emits: []
has_state: false
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/form-field.tsx"
---
## Ejemplo
```tsx
<FormField label="Email" helperText="Tu email corporativo" error={errors.email}>
<Input type="email" />
</FormField>
```
+42
View File
@@ -0,0 +1,42 @@
import * as React from "react"
import { cn } from "../core/cn"
interface FormFieldProps {
label?: string
helperText?: string
error?: string
children: React.ReactNode
className?: string
}
function FormField({ label, helperText, error, children, className }: FormFieldProps) {
const id = React.useId()
const inputId = `${id}-input`
const helperId = `${id}-helper`
const errorId = `${id}-error`
const describedBy = [helperText ? helperId : null, error ? errorId : null].filter(Boolean).join(" ") || undefined
const childWithProps = React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child as React.ReactElement<Record<string, unknown>>, {
id: inputId,
"aria-invalid": error ? true : undefined,
"aria-describedby": describedBy,
})
}
return child
})
return (
<div className={cn("flex flex-col gap-1.5", className)}>
{label && <label htmlFor={inputId} className="text-sm font-medium text-foreground">{label}</label>}
{childWithProps}
{helperText && !error && <p id={helperId} className="text-sm text-muted-foreground">{helperText}</p>}
{error && <p id={errorId} className="text-sm text-destructive">{error}</p>}
</div>
)
}
export { FormField }
export type { FormFieldProps }
+51
View File
@@ -0,0 +1,51 @@
---
name: input
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Input(props: InputHTMLAttributes): JSX.Element"
description: "Campo de entrada accesible con soporte para iconos, grupos, validación ARIA y estados disabled/invalid."
tags: [input, form, component, ui, interactive]
uses_functions: [cn_typescript_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", "react"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/input.tsx"
props:
- name: type
type: "string"
required: false
description: "Tipo de input HTML"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: [onChange, onFocus, onBlur]
has_state: false
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/input.tsx"
---
## Ejemplo
```tsx
<Input placeholder="Email" type="email" />
<InputGroup>
<InputIcon position="start"><SearchIcon /></InputIcon>
<Input placeholder="Buscar..." />
</InputGroup>
```
## Notas
Exporta Input, InputGroup e InputIcon. InputGroup detecta automáticamente la presencia de iconos y ajusta padding del Input.
+56
View File
@@ -0,0 +1,56 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "../core/cn"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground 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 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"group-has-[data-slot=input-icon-start]/input-group:pl-9",
"group-has-[data-slot=input-icon-end]/input-group:pr-9",
className
)}
{...props}
/>
)
}
interface InputGroupProps {
children: React.ReactNode
className?: string
}
function InputGroup({ children, className }: InputGroupProps) {
return (
<div data-slot="input-group" className={cn("group/input-group relative", className)}>
{children}
</div>
)
}
interface InputIconProps {
children: React.ReactNode
position: "start" | "end"
className?: string
}
function InputIcon({ children, position, className }: InputIconProps) {
return (
<span
data-slot={`input-icon-${position}`}
className={cn(
"pointer-events-none absolute top-1/2 -translate-y-1/2 text-muted-foreground [&_svg]:size-4",
position === "start" && "left-2.5",
position === "end" && "right-2.5",
className
)}
>
{children}
</span>
)
}
export { Input, InputGroup, InputIcon }
+56
View File
@@ -0,0 +1,56 @@
---
name: kpi_card
kind: component
lang: typescript
domain: ui
version: "1.0.0"
purity: impure
signature: "KPICard(props: KPICardProps): JSX.Element"
description: "Card de KPI con label, valor, delta porcentual con color semántico, icono y subtítulo. 3 tamaños."
tags: [kpi, card, metrics, dashboard, component, ui]
uses_functions: [cn_typescript_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/kpi_card.tsx"
props:
- name: label
type: "string"
required: true
description: "Etiqueta del KPI"
- name: value
type: "string | number"
required: true
description: "Valor principal"
- name: delta
type: "{ value: number; isPositive: boolean }"
required: false
description: "Cambio porcentual con dirección"
- name: icon
type: "ReactNode"
required: false
description: "Icono decorativo"
- name: size
type: "'sm' | 'default' | 'lg'"
required: false
description: "Tamaño"
emits: []
has_state: false
framework: react
variant: [sm, default, lg]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/kpi-card.tsx"
---
## Ejemplo
```tsx
<KPICard label="Revenue" value="$12,450" delta={{ value: 12.5, isPositive: true }} />
<KPICard label="Users" value={1234} size="lg" icon={<UsersIcon />} />
```
+60
View File
@@ -0,0 +1,60 @@
import * as React from 'react'
import { cn } from '../core/cn'
type KPICardSize = 'sm' | 'default' | 'lg'
interface Delta {
value: number
isPositive: boolean
}
interface KPICardProps extends React.HTMLAttributes<HTMLDivElement> {
label: string
value: string | number
delta?: Delta
icon?: React.ReactNode
subtitle?: string
size?: KPICardSize
}
const sizeStyles: Record<KPICardSize, { value: string; label: string }> = {
sm: { value: 'text-2xl font-bold', label: 'text-xs' },
default: { value: 'text-3xl font-bold', label: 'text-sm' },
lg: { value: 'text-4xl font-bold', label: 'text-base' },
}
const KPICard = React.forwardRef<HTMLDivElement, KPICardProps>(
({ label, value, delta, icon, subtitle, size = 'default', className, ...props }, ref) => {
const styles = sizeStyles[size]
const deltaColor = delta
? delta.value === 0 ? 'text-muted-foreground'
: delta.isPositive ? 'text-green-600 dark:text-green-500'
: 'text-red-600 dark:text-red-500'
: ''
return (
<div ref={ref} className={cn('rounded-lg border bg-card p-4 text-card-foreground shadow-sm', className)} {...props}>
<div className="flex items-start justify-between">
<div className="space-y-1">
<p className={cn('text-muted-foreground', styles.label)}>{label}</p>
{subtitle && <p className="text-xs text-muted-foreground/80">{subtitle}</p>}
</div>
{icon && <div className="text-muted-foreground">{icon}</div>}
</div>
<div className="mt-3 flex items-end justify-between gap-4">
<div className="space-y-1">
<p className={cn('tracking-tight', styles.value)}>{value}</p>
{delta && (
<div className={cn('flex items-center gap-1 text-sm font-medium', deltaColor)}>
<span>{delta.value > 0 ? '+' : ''}{delta.value}%</span>
</div>
)}
</div>
</div>
</div>
)
}
)
KPICard.displayName = 'KPICard'
export { KPICard, type KPICardProps, type Delta, type KPICardSize }
+39
View File
@@ -0,0 +1,39 @@
---
name: label
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Label(props: LabelHTMLAttributes): JSX.Element"
description: "Etiqueta de formulario accesible con soporte para estados disabled y peer-disabled."
tags: [label, form, component, ui]
uses_functions: [cn_typescript_core]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["react"]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/label.tsx"
props:
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/label.tsx"
---
## Ejemplo
```tsx
<Label htmlFor="email">Email</Label>
```
+17
View File
@@ -0,0 +1,17 @@
import * as React from "react"
import { cn } from "../core/cn"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }
+68
View File
@@ -0,0 +1,68 @@
---
name: line_chart
kind: component
lang: typescript
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]
returns: []
returns_optional: false
error_type: ""
imports: [recharts]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/line_chart.tsx"
props:
- name: data
type: "Record<string, unknown>[]"
required: true
description: "Array de datos"
- name: xKey
type: "string"
required: true
description: "Key del eje X"
- name: series
type: "Series[]"
required: false
description: "Series de datos"
- name: zoomable
type: "boolean"
required: false
description: "Habilitar zoom brush"
- name: curveType
type: "'linear' | 'monotone' | 'step' | 'stepBefore' | 'stepAfter'"
required: false
description: "Tipo de curva (default monotone)"
- name: referenceLines
type: "Array<{ y: number; label?: string; color?: string }>"
required: false
description: "Líneas de referencia horizontales"
emits: []
has_state: false
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/charts/line-chart.tsx"
---
## Ejemplo
```tsx
<LineChart
data={salesData}
xKey="month"
series={[
{ key: "revenue", name: "Revenue", color: "#3b82f6" },
{ key: "cost", name: "Cost", color: "#ef4444" },
]}
zoomable
showLegend
/>
```
+57
View File
@@ -0,0 +1,57 @@
import {
LineChart as RechartsLineChart, Line, XAxis, YAxis, CartesianGrid,
Tooltip, Legend, Brush, ReferenceLine,
} from 'recharts'
import { ChartContainer, ChartTooltipContent, type Series, getSeriesColor } from './chart_container'
type CurveType = 'linear' | 'monotone' | 'step' | 'stepBefore' | 'stepAfter'
interface LineChartProps {
data: Record<string, unknown>[]
xKey: string
yKey?: string
series?: Series[]
curveType?: CurveType
showGrid?: boolean
showLegend?: boolean
showDots?: boolean
zoomable?: boolean
height?: number | string
className?: string
xAxisFormatter?: (value: unknown) => string
yAxisFormatter?: (value: unknown) => string
valueFormatter?: (value: number) => string
referenceLines?: Array<{ y: number; label?: string; color?: string }>
}
function LineChartComponent({
data, xKey, yKey, series, curveType = 'monotone', showGrid = true, showLegend = false,
showDots = true, zoomable = false, height = 300, className, xAxisFormatter, yAxisFormatter,
valueFormatter = (v) => v.toLocaleString(), referenceLines = [],
}: LineChartProps) {
const lines = series
? series.map((s, i) => ({ dataKey: s.key, name: s.name, stroke: getSeriesColor(i, s.color) }))
: yKey ? [{ dataKey: yKey, name: yKey, stroke: getSeriesColor(0) }] : []
return (
<ChartContainer className={className} height={height}>
<RechartsLineChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: zoomable ? 30 : 10 }}>
{showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />}
<XAxis dataKey={xKey} tickFormatter={xAxisFormatter} className="text-xs fill-muted-foreground" />
<YAxis tickFormatter={yAxisFormatter} className="text-xs fill-muted-foreground" />
<Tooltip content={<ChartTooltipContent valueFormatter={valueFormatter} />} cursor={{ stroke: 'hsl(var(--muted-foreground))', strokeDasharray: '3 3' }} />
{showLegend && <Legend />}
{referenceLines.map((ref, i) => (
<ReferenceLine key={i} y={ref.y} stroke={ref.color || 'hsl(var(--muted-foreground))'} strokeDasharray="3 3" label={ref.label ? { value: ref.label, position: 'right' } : undefined} />
))}
{lines.map((line) => (
<Line key={line.dataKey} type={curveType} dataKey={line.dataKey} name={line.name} stroke={line.stroke} strokeWidth={2} dot={showDots ? { r: 3, fill: line.stroke } : false} activeDot={{ r: 5, fill: line.stroke }} />
))}
{zoomable && <Brush dataKey={xKey} height={20} stroke="hsl(var(--primary))" fill="hsl(var(--muted))" tickFormatter={xAxisFormatter} />}
</RechartsLineChart>
</ChartContainer>
)
}
export const LineChart = LineChartComponent
export type { LineChartProps, CurveType }
+62
View File
@@ -0,0 +1,62 @@
---
name: page_header
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/page_header.tsx"
props:
- name: title
type: "string"
required: true
description: "Título principal"
- name: subtitle
type: "string"
required: false
description: "Subtítulo"
- name: actions
type: "ReactNode"
required: false
description: "Botones de acción"
- name: tabs
type: "TabItem[]"
required: false
description: "Tabs de navegación integrados"
- name: sticky
type: "boolean"
required: false
description: "Header fijo al scroll"
emits: [onBack, onTabChange]
has_state: false
framework: react
variant: [full, simple]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/page-header.tsx"
---
## Ejemplo
```tsx
<PageHeader
title="Dashboard"
subtitle="Vista general"
actions={<Button>Export</Button>}
tabs={[{ label: "Overview", value: "overview" }, { label: "Analytics", value: "analytics" }]}
activeTab="overview"
onTabChange={setTab}
/>
```
+94
View File
@@ -0,0 +1,94 @@
"use client"
import * as React from "react"
import { cn } from "../core/cn"
interface TabItem {
label: string
value: string
icon?: React.ReactNode
disabled?: boolean
}
interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
title: string
subtitle?: string
actions?: React.ReactNode
onBack?: () => void
tabs?: TabItem[]
activeTab?: string
onTabChange?: (value: string) => void
badge?: React.ReactNode
sticky?: boolean
}
function PageHeader({
title, subtitle, actions, onBack, tabs, activeTab, onTabChange,
badge, sticky = false, className, ...props
}: PageHeaderProps) {
return (
<header
data-slot="page-header"
className={cn("space-y-4 border-b bg-background pb-4", sticky && "sticky top-0 z-20", className)}
{...props}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3">
{onBack && (
<button onClick={onBack} className="mt-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-muted">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m15 18-6-6 6-6"/></svg>
</button>
)}
<div className="space-y-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{badge}
</div>
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
</div>
</div>
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
</div>
{tabs && tabs.length > 0 && (
<nav className="flex gap-4 border-b -mb-4 pb-0">
{tabs.map((tab) => (
<button
key={tab.value}
type="button"
disabled={tab.disabled}
onClick={() => onTabChange?.(tab.value)}
className={cn(
"inline-flex items-center gap-2 border-b-2 px-1 pb-3 text-sm font-medium transition-colors",
activeTab === tab.value ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground",
tab.disabled && "pointer-events-none opacity-50"
)}
>
{tab.icon && <span className="size-4">{tab.icon}</span>}
{tab.label}
</button>
))}
</nav>
)}
</header>
)
}
interface SimplePageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
title: string
description?: string
children?: React.ReactNode
}
function SimplePageHeader({ title, description, children, className, ...props }: SimplePageHeaderProps) {
return (
<div className={cn("flex items-center justify-between border-b pb-4", className)} {...props}>
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{description && <p className="text-sm text-muted-foreground">{description}</p>}
</div>
{children && <div className="flex items-center gap-2">{children}</div>}
</div>
)
}
export { PageHeader, SimplePageHeader }
+81
View File
@@ -0,0 +1,81 @@
---
name: progress_bar
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [class-variance-authority]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/progress_bar.tsx"
props:
- name: value
type: "number"
required: true
description: "Valor actual de progreso"
- name: max
type: "number"
required: false
description: "Valor máximo (default 100)"
- name: buffer
type: "number"
required: false
description: "Valor de buffer secundario (opcional)"
- name: showValue
type: "boolean"
required: false
description: "Mostrar porcentaje como texto superpuesto"
- name: animated
type: "boolean"
required: false
description: "Activar animación de rayas (stripes)"
- name: indeterminate
type: "boolean"
required: false
description: "Modo indeterminado sin valor conocido"
- name: size
type: "'sm' | 'md' | 'lg'"
required: false
description: "Altura de la barra (default md)"
- name: color
type: "'primary' | 'success' | 'warning' | 'destructive'"
required: false
description: "Color semántico (default primary)"
- name: label
type: "string"
required: false
description: "aria-label para accesibilidad"
emits: []
has_state: false
framework: react
variant: [primary, success, warning, destructive]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/progress/progress-bar.tsx"
---
## Ejemplo
```tsx
<ProgressBar value={75} color="success" showValue />
<ProgressBar value={0} indeterminate />
<ProgressBar value={50} buffer={80} animated />
<ProgressBar value={30} size="lg" color="warning" />
```
## Notas
El porcentaje se clampea a [0, 100] internamente. El buffer se renderiza como capa semitransparente detrás del fill.
Las animaciones de stripes e indeterminate requieren keyframes definidos en el CSS global:
- `progress-stripes`: background-position de 0 a 1rem
- `progress-indeterminate`: translateX de -100% a 300%
+68
View File
@@ -0,0 +1,68 @@
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../core/cn'
const progressBarVariants = cva(
'relative w-full overflow-hidden rounded-full bg-muted',
{
variants: {
size: { sm: 'h-1', md: 'h-2', lg: 'h-3' },
color: {
primary: '[&_.progress-fill]:bg-primary',
success: '[&_.progress-fill]:bg-emerald-500',
warning: '[&_.progress-fill]:bg-amber-500',
destructive: '[&_.progress-fill]:bg-destructive',
},
},
defaultVariants: { size: 'md', color: 'primary' },
}
)
export interface ProgressBarProps extends VariantProps<typeof progressBarVariants> {
value: number
max?: number
buffer?: number
showValue?: boolean
animated?: boolean
indeterminate?: boolean
label?: string
className?: string
}
export function ProgressBar({
value, max = 100, buffer, showValue = false, animated = false,
indeterminate = false, size = 'md', color = 'primary', label, className,
}: ProgressBarProps) {
const percentage = Math.min(100, Math.max(0, (value / max) * 100))
const bufferPercentage = buffer ? Math.min(100, Math.max(0, (buffer / max) * 100)) : undefined
return (
<div
data-slot="progress-bar"
role="progressbar"
aria-valuenow={indeterminate ? undefined : value}
aria-valuemin={0}
aria-valuemax={max}
aria-label={label}
className={cn(progressBarVariants({ size, color }), className)}
>
{bufferPercentage !== undefined && (
<div className="progress-buffer absolute inset-0 bg-current opacity-20 transition-all duration-300" style={{ width: `${bufferPercentage}%` }} />
)}
<div
className={cn(
'progress-fill h-full transition-all duration-300',
animated && 'bg-[length:1rem_1rem] bg-[linear-gradient(45deg,rgba(255,255,255,.15)_25%,transparent_25%,transparent_50%,rgba(255,255,255,.15)_50%,rgba(255,255,255,.15)_75%,transparent_75%,transparent)] animate-[progress-stripes_1s_linear_infinite]',
indeterminate && 'w-1/3 animate-[progress-indeterminate_1.5s_ease-in-out_infinite]'
)}
style={indeterminate ? undefined : { width: `${percentage}%` }}
/>
{showValue && !indeterminate && (
<span className="absolute inset-0 flex items-center justify-center text-[10px] font-medium text-foreground mix-blend-difference">
{Math.round(percentage)}%
</span>
)}
</div>
)
}
export { progressBarVariants }
+68
View File
@@ -0,0 +1,68 @@
---
name: select
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", lucide-react, react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/select.tsx"
props:
- name: value
type: "T"
required: false
description: "Valor seleccionado (controlled)"
- name: onValueChange
type: "(value: T) => void"
required: false
description: "Callback al cambiar selección"
- name: defaultValue
type: "T"
required: false
description: "Valor inicial (uncontrolled)"
- name: disabled
type: "boolean"
required: false
description: "Deshabilitar el select"
emits: [onValueChange]
has_state: true
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/select.tsx"
---
## Ejemplo
```tsx
<Select>
<SelectTrigger><SelectValue placeholder="Elegir..." /></SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectGroupLabel>Frutas</SelectGroupLabel>
<SelectItem value="apple">Manzana</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectItem value="other">Otro</SelectItem>
</SelectContent>
</Select>
```
## Notas
Exporta 9 subcomponentes composables: Select, SelectTrigger, SelectValue, SelectPortal, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectSeparator.
Genérico sobre el tipo de valor T — TypeScript infiere el tipo desde el prop value/defaultValue.
Depende de @base-ui/react y lucide-react.
+88
View File
@@ -0,0 +1,88 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { ChevronDown, Check } from "lucide-react"
import { cn } from "../core/cn"
function Select<T>({ ...props }: SelectPrimitive.Root.Props<T>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectValue({ ...props }: SelectPrimitive.Value.Props) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({ className, children, ...props }: SelectPrimitive.Trigger.Props) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
className={cn(
"flex h-8 w-full items-center justify-between gap-2 rounded-lg border border-input bg-transparent px-2.5 py-1 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:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon>
<ChevronDown className="size-4 shrink-0 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectPortal({ ...props }: SelectPrimitive.Portal.Props) {
return <SelectPrimitive.Portal data-slot="select-portal" {...props} />
}
function SelectContent({ className, children, ...props }: SelectPrimitive.Positioner.Props) {
return (
<SelectPortal>
<SelectPrimitive.Positioner
data-slot="select-content"
className={cn(
"relative z-50 max-h-[300px] min-w-[8rem] overflow-hidden rounded-lg border bg-popover 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
)}
sideOffset={4}
{...props}
>
<SelectPrimitive.Popup className="w-full p-1">{children}</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPortal>
)
}
function SelectGroup({ ...props }: SelectPrimitive.Group.Props) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectGroupLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
return <SelectPrimitive.GroupLabel data-slot="select-group-label" className={cn("px-2 py-1.5 text-xs font-medium text-muted-foreground", className)} {...props} />
}
function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none 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">
<SelectPrimitive.ItemIndicator><Check className="size-4" /></SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="select-separator" className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
}
export { Select, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectPortal, SelectSeparator, SelectTrigger, SelectValue }
+57
View File
@@ -0,0 +1,57 @@
---
name: settings_page
kind: function
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/settings_page.tsx"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: ""
---
## Ejemplo
```tsx
settingsPage({
title: 'Project Settings',
sections: [
{
id: 'general',
title: 'General',
description: 'Basic project configuration',
fields: [
{ key: 'name', label: 'Project Name', type: 'text', value: 'My Project' },
{ key: 'visibility', label: 'Public', description: 'Make project visible to everyone', type: 'toggle', value: true },
{ key: 'language', label: 'Language', type: 'select', options: [{ label: 'English', value: 'en' }, { label: 'Spanish', value: 'es' }] },
],
},
{
id: 'notifications',
title: 'Notifications',
fields: [
{ key: 'email', label: 'Email notifications', type: 'toggle', value: false },
{ key: 'webhook', label: 'Webhook URL', type: 'text', placeholder: 'https://...' },
],
},
],
onSave: handleSave,
})
```
## Notas
Layout de settings estándar con sidebar de navegación (oculta en mobile). Secciones con anclas para scroll. Soporta 5 tipos de campo.
+112
View File
@@ -0,0 +1,112 @@
import * as React from 'react'
import { cn } from '../core/cn'
interface SettingField {
key: string
label: string
description?: string
type: 'text' | 'number' | 'toggle' | 'select' | 'textarea'
value?: unknown
options?: Array<{ label: string; value: string }>
placeholder?: string
}
interface SettingSection {
id: string
title: string
description?: string
fields: SettingField[]
}
interface SettingsPageProps {
title?: string
subtitle?: string
sections: SettingSection[]
onSave?: (values: Record<string, unknown>) => void
className?: string
}
export function settingsPage({
title = 'Settings',
subtitle,
sections,
onSave,
className,
}: SettingsPageProps): React.ReactElement {
return (
<div className={cn('space-y-6', className)}>
{/* Header */}
<div className="border-b pb-4">
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
</div>
{/* Tabs navigation */}
<div className="flex gap-6">
<nav className="hidden w-48 shrink-0 md:block">
<div className="space-y-1">
{sections.map((section) => (
<a
key={section.id}
href={`#${section.id}`}
className="block rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground"
>
{section.title}
</a>
))}
</div>
</nav>
{/* Sections */}
<div className="flex-1 space-y-8">
{sections.map((section) => (
<div key={section.id} id={section.id} className="space-y-4">
<div>
<h2 className="text-lg font-medium">{section.title}</h2>
{section.description && <p className="text-sm text-muted-foreground">{section.description}</p>}
</div>
<div className="space-y-4 rounded-lg border p-4">
{section.fields.map((field) => (
<div key={field.key} className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-0.5">
<label className="text-sm font-medium">{field.label}</label>
{field.description && <p className="text-xs text-muted-foreground">{field.description}</p>}
</div>
<div className="w-full sm:w-64">
{field.type === 'toggle' ? (
<button className={cn(
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors',
field.value ? 'bg-primary' : 'bg-input'
)}>
<span className={cn('pointer-events-none block size-4 rounded-full bg-background shadow-lg ring-0 transition-transform', field.value ? 'translate-x-4' : 'translate-x-0')} />
</button>
) : field.type === 'select' ? (
<select className="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm">
{field.options?.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
) : field.type === 'textarea' ? (
<textarea className="w-full rounded-lg border border-input bg-transparent px-2.5 py-1.5 text-sm" rows={3} placeholder={field.placeholder} defaultValue={String(field.value ?? '')} />
) : (
<input type={field.type} className="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm" placeholder={field.placeholder} defaultValue={String(field.value ?? '')} />
)}
</div>
</div>
))}
</div>
</div>
))}
{onSave && (
<div className="flex justify-end border-t pt-4">
<button className="inline-flex h-8 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/80">
Save changes
</button>
</div>
)}
</div>
</div>
</div>
)
}
export type { SettingsPageProps, SettingSection, SettingField }
+65
View File
@@ -0,0 +1,65 @@
---
name: skeleton
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/skeleton.tsx"
props:
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
- name: lines
type: "number"
required: false
description: "Número de líneas (SkeletonText, default 3)"
- name: rows
type: "number"
required: false
description: "Filas de tabla (SkeletonTable, default 5)"
- name: columns
type: "number"
required: false
description: "Columnas de tabla (SkeletonTable, default 4)"
- name: size
type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'"
required: false
description: "Tamaño del avatar (SkeletonAvatar, default md)"
emits: []
has_state: false
framework: react
variant: [base, text, card, avatar, button, table]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/skeleton.tsx"
---
## Ejemplo
```tsx
<SkeletonCard />
<SkeletonText lines={4} />
<SkeletonTable rows={10} columns={5} />
<SkeletonAvatar size="lg" />
<SkeletonButton />
```
## Notas
Exporta 6 variantes preconfiguradas. Todas componen sobre el Skeleton base con animate-pulse.
La última línea de SkeletonText se acorta a w-4/5 para simular texto real.
SkeletonCard incluye imagen (h-[180px]) + dos líneas de texto.
+54
View File
@@ -0,0 +1,54 @@
import * as React from "react"
import { cn } from "../core/cn"
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div data-slot="skeleton" className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
}
function SkeletonText({ className, lines = 3, ...props }: React.HTMLAttributes<HTMLDivElement> & { lines?: number }) {
return (
<div className={cn("space-y-2", className)} {...props}>
{Array.from({ length: lines }).map((_, i) => (
<Skeleton key={i} className={cn("h-4", i === lines - 1 ? "w-4/5" : "w-full")} />
))}
</div>
)
}
function SkeletonCard({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div className={cn("space-y-3", className)} {...props}>
<Skeleton className="h-[180px] w-full rounded-lg" />
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-4 w-1/2" />
</div>
)
}
function SkeletonAvatar({ className, size = "md", ...props }: React.HTMLAttributes<HTMLDivElement> & { size?: "xs" | "sm" | "md" | "lg" | "xl" }) {
const sizeClasses = { xs: "size-6", sm: "size-8", md: "size-10", lg: "size-12", xl: "size-16" }
return <Skeleton className={cn("rounded-full", sizeClasses[size], className)} {...props} />
}
function SkeletonButton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <Skeleton className={cn("h-8 w-24 rounded-lg", className)} {...props} />
}
function SkeletonTable({ className, rows = 5, columns = 4, ...props }: React.HTMLAttributes<HTMLDivElement> & { rows?: number; columns?: number }) {
return (
<div className={cn("space-y-3", className)} {...props}>
<div className="flex gap-4">
{Array.from({ length: columns }).map((_, i) => <Skeleton key={i} className="h-4 flex-1" />)}
</div>
<div className="space-y-2">
{Array.from({ length: rows }).map((_, rowIndex) => (
<div key={rowIndex} className="flex gap-4">
{Array.from({ length: columns }).map((_, colIndex) => <Skeleton key={colIndex} className="h-4 flex-1" />)}
</div>
))}
</div>
</div>
)
}
export { Skeleton, SkeletonAvatar, SkeletonButton, SkeletonCard, SkeletonTable, SkeletonText }
+60
View File
@@ -0,0 +1,60 @@
---
name: sparkline
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/sparkline.tsx"
props:
- name: data
type: "number[]"
required: true
description: "Array de valores numéricos"
- name: variant
type: "'line' | 'area' | 'bar'"
required: false
description: "Tipo de visualización"
- name: color
type: "string"
required: false
description: "Color del gráfico"
- name: width
type: "number"
required: false
description: "Ancho en px (default 80)"
- name: height
type: "number"
required: false
description: "Alto en px (default 24)"
emits: []
has_state: false
framework: react
variant: [line, area, bar]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/sparkline.tsx"
---
## Ejemplo
```tsx
<Sparkline data={[10, 25, 15, 40, 30, 55]} variant="area" color="#22c55e" />
<Sparkline data={[5, 3, 8, 1, 9]} variant="bar" width={60} height={20} />
```
## Notas
SVG puro — sin dependencia de Recharts. Ideal para inline en tablas y cards.
+72
View File
@@ -0,0 +1,72 @@
import * as React from 'react'
import { cn } from '../core/cn'
type SparklineVariant = 'line' | 'area' | 'bar'
interface SparklineProps extends React.SVGAttributes<SVGSVGElement> {
data: number[]
variant?: SparklineVariant
color?: string
width?: number
height?: number
strokeWidth?: number
showLastPoint?: boolean
}
function getPath(data: number[], width: number, height: number, padding: number = 2) {
if (data.length === 0) return { linePath: '', areaPath: '' }
const min = Math.min(...data)
const max = Math.max(...data)
const range = max - min || 1
const ew = width - padding * 2
const eh = height - padding * 2
const points = data.map((value, index) => ({
x: padding + (index / (data.length - 1)) * ew,
y: padding + eh - ((value - min) / range) * eh,
}))
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')
const areaPath = `${linePath} L ${points[points.length - 1].x} ${height - padding} L ${padding} ${height - padding} Z`
return { linePath, areaPath }
}
const Sparkline = React.forwardRef<SVGSVGElement, SparklineProps>(
({ data, variant = 'line', color = 'currentColor', width = 80, height = 24, strokeWidth = 1.5, showLastPoint = true, className, ...props }, ref) => {
if (data.length === 0) return <svg ref={ref} width={width} height={height} viewBox={`0 0 ${width} ${height}`} className={cn('text-primary', className)} {...props} />
if (variant === 'bar') {
const min = Math.min(...data, 0)
const max = Math.max(...data)
const range = max - min || 1
const p = 2
const eh = height - p * 2
const bw = (width - p * 2) / data.length - 1
return (
<svg ref={ref} width={width} height={height} viewBox={`0 0 ${width} ${height}`} className={cn('text-primary', className)} {...props}>
{data.map((value, index) => {
const bh = ((value - min) / range) * eh
const x = p + index * ((width - p * 2) / data.length) + 0.5
const y = p + eh - bh
return <rect key={index} x={x} y={y} width={Math.max(bw, 1)} height={Math.max(bh, 1)} fill={color} opacity={0.8} />
})}
</svg>
)
}
const { linePath, areaPath } = getPath(data, width, height)
const lastPoint = {
x: width - 2,
y: 2 + (height - 4) - ((data[data.length - 1] - Math.min(...data)) / (Math.max(...data) - Math.min(...data) || 1)) * (height - 4)
}
return (
<svg ref={ref} width={width} height={height} viewBox={`0 0 ${width} ${height}`} className={cn('text-primary', className)} {...props}>
{variant === 'area' && <path d={areaPath} fill={color} opacity={0.2} />}
<path d={linePath} fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" />
{showLastPoint && <circle cx={lastPoint.x} cy={lastPoint.y} r={2.5} fill={color} />}
</svg>
)
}
)
Sparkline.displayName = 'Sparkline'
export { Sparkline, type SparklineProps, type SparklineVariant }
+50
View File
@@ -0,0 +1,50 @@
---
name: tabs
kind: component
lang: typescript
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_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/tabs.tsx"
props:
- name: orientation
type: "'horizontal' | 'vertical'"
required: false
description: "Orientación de los tabs"
- name: variant
type: "'default' | 'line'"
required: false
description: "Estilo visual de la lista"
emits: [onValueChange]
has_state: true
framework: react
variant: [default, line]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/tabs.tsx"
---
## Ejemplo
```tsx
<Tabs defaultValue="general">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
</TabsList>
<TabsContent value="general">...</TabsContent>
<TabsContent value="security">...</TabsContent>
</Tabs>
```
+43
View File
@@ -0,0 +1,43 @@
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
function Tabs({ className, orientation = "horizontal", ...props }: TabsPrimitive.Root.Props) {
return (
<TabsPrimitive.Root data-slot="tabs" data-orientation={orientation} className={cn("group/tabs flex gap-2 data-horizontal:flex-col", className)} {...props} />
)
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: { default: "bg-muted", line: "gap-1 bg-transparent" },
},
defaultVariants: { variant: "default" },
}
)
function TabsList({ className, variant = "default", ...props }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
return <TabsPrimitive.List data-slot="tabs-list" data-variant={variant} className={cn(tabsListVariants({ variant }), className)} {...props} />
}
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
return (
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30",
className
)}
{...props}
/>
)
}
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
return <TabsPrimitive.Panel data-slot="tabs-content" className={cn("flex-1 text-sm outline-none", className)} {...props} />
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
+65
View File
@@ -0,0 +1,65 @@
---
name: theme_provider
kind: component
lang: typescript
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]
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/theme_provider.tsx"
props:
- name: themes
type: "Record<string, Theme>"
required: true
description: "Mapa de temas disponibles"
- name: defaultTheme
type: "string"
required: false
description: "Nombre del tema por defecto"
- name: children
type: "ReactNode"
required: true
description: "Contenido de la app"
emits: []
has_state: true
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/hooks/use-theme.tsx"
---
## Ejemplo
```tsx
import { ThemeProvider, useTheme } from './theme_provider'
<ThemeProvider themes={allThemes} defaultTheme="dark">
<App />
</ThemeProvider>
// Dentro de un componente:
function ThemeSwitcher() {
const { themeName, setTheme, themes } = useTheme()
return (
<select value={themeName} onChange={(e) => setTheme(e.target.value)}>
{Object.values(themes).map(t => <option key={t.name} value={t.name}>{t.label}</option>)}
</select>
)
}
```
## Notas
Detecta prefers-color-scheme automáticamente. Persiste elección en localStorage. Exporta ThemeProvider (componente) y useTheme (hook).
+101
View File
@@ -0,0 +1,101 @@
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react'
interface ThemeColors {
[key: string]: string
}
interface Theme {
name: string
label: string
colors: ThemeColors
}
interface ThemeContextValue {
theme: Theme
themeName: string
setTheme: (name: string) => void
themes: Record<string, Theme>
}
const STORAGE_KEY = 'frontend-library-theme'
const ThemeContext = createContext<ThemeContextValue | null>(null)
function applyThemeColors(theme: Theme) {
const root = document.documentElement
const cssVarMap: Record<string, string> = {
background: '--background', foreground: '--foreground',
card: '--card', cardForeground: '--card-foreground',
popover: '--popover', popoverForeground: '--popover-foreground',
primary: '--primary', primaryForeground: '--primary-foreground',
secondary: '--secondary', secondaryForeground: '--secondary-foreground',
muted: '--muted', mutedForeground: '--muted-foreground',
accent: '--accent', accentForeground: '--accent-foreground',
destructive: '--destructive', destructiveForeground: '--destructive-foreground',
success: '--success', successForeground: '--success-foreground',
warning: '--warning', warningForeground: '--warning-foreground',
info: '--info', infoForeground: '--info-foreground',
surface: '--surface', surfaceHover: '--surface-hover', overlay: '--overlay',
border: '--border', input: '--input', ring: '--ring',
chart1: '--chart-1', chart2: '--chart-2', chart3: '--chart-3', chart4: '--chart-4', chart5: '--chart-5',
sidebar: '--sidebar', sidebarForeground: '--sidebar-foreground',
sidebarPrimary: '--sidebar-primary', sidebarPrimaryForeground: '--sidebar-primary-foreground',
sidebarAccent: '--sidebar-accent', sidebarAccentForeground: '--sidebar-accent-foreground',
sidebarBorder: '--sidebar-border', sidebarRing: '--sidebar-ring',
}
Object.entries(cssVarMap).forEach(([key, cssVar]) => {
if (theme.colors[key]) root.style.setProperty(cssVar, theme.colors[key])
})
if (theme.name === 'dark' || theme.name === 'midnight' || theme.name === 'sunset') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
}
interface ThemeProviderProps {
children: ReactNode
themes: Record<string, Theme>
defaultTheme?: string
}
export function ThemeProvider({ children, themes, defaultTheme: initialTheme }: ThemeProviderProps) {
const [themeName, setThemeName] = useState<string>(() => {
if (initialTheme) return initialTheme
if (typeof window === 'undefined') return 'default'
const stored = localStorage.getItem(STORAGE_KEY)
if (stored && themes[stored]) return stored
if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark'
return 'default'
})
const theme = themes[themeName] ?? themes['default'] ?? Object.values(themes)[0]
const setTheme = useCallback((name: string) => {
if (themes[name]) {
setThemeName(name)
localStorage.setItem(STORAGE_KEY, name)
}
}, [themes])
useEffect(() => { applyThemeColors(theme) }, [theme])
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)')
const handle = (e: MediaQueryListEvent) => {
if (!localStorage.getItem(STORAGE_KEY)) setThemeName(e.matches ? 'dark' : 'default')
}
mq.addEventListener('change', handle)
return () => mq.removeEventListener('change', handle)
}, [])
return <ThemeContext.Provider value={{ theme, themeName, setTheme, themes }}>{children}</ThemeContext.Provider>
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider')
return ctx
}
export { ThemeContext }
export type { ThemeContextValue, ThemeProviderProps, Theme, ThemeColors }
+51
View File
@@ -0,0 +1,51 @@
---
name: tooltip
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/tooltip.tsx"
props:
- name: delayDuration
type: "number"
required: false
description: "Delay en ms antes de mostrar el tooltip (default 300)"
emits: []
has_state: true
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/tooltip.tsx"
---
## Ejemplo
```tsx
<TooltipProvider>
<Tooltip>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>Tooltip text</TooltipContent>
</Tooltip>
</TooltipProvider>
```
## Notas
Exporta 5 subcomponentes: TooltipProvider, Tooltip, TooltipTrigger, TooltipPortal, TooltipContent.
TooltipProvider gestiona el delay global — envolver la app o sección con un único Provider.
TooltipContent incluye Arrow con fill-primary automático.
Depende de @base-ui/react — asegurarse de que está en package.json del frontend.
+45
View File
@@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
import { cn } from "../core/cn"
function TooltipProvider({ delayDuration = 300, ...props }: { delayDuration?: number; children: React.ReactNode }) {
return <TooltipPrimitive.Provider delay={delayDuration} {...props} />
}
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipPortal({ ...props }: TooltipPrimitive.Portal.Props) {
return <TooltipPrimitive.Portal data-slot="tooltip-portal" {...props} />
}
function TooltipContent({ className, sideOffset = 4, ...props }: TooltipPrimitive.Positioner.Props) {
return (
<TooltipPortal>
<TooltipPrimitive.Positioner
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground",
"animate-in fade-in-0 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}
>
<TooltipPrimitive.Popup>{props.children}</TooltipPrimitive.Popup>
<TooltipPrimitive.Arrow className="fill-primary" />
</TooltipPrimitive.Positioner>
</TooltipPortal>
)
}
export { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger }
@@ -0,0 +1,57 @@
---
name: use_animated_canvas
kind: component
lang: typescript
domain: ui
version: "1.0.0"
purity: impure
signature: "useAnimatedCanvas(options: AnimatedCanvasOptions): AnimatedCanvasResult"
description: "Hook React para canvas animado a N fps via requestAnimationFrame. Maneja DPR, resize, throttling, y contador de FPS real."
tags: [canvas, animation, fps, requestAnimationFrame, hook, realtime, component, ui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/use_animated_canvas.tsx"
props:
- name: fps
type: "number"
required: false
description: "Target FPS (default 60)"
- name: draw
type: "(ctx: CanvasRenderingContext2D, width: number, height: number, frameCount: number) => void"
required: true
description: "Callback de renderizado"
emits: []
has_state: true
framework: react
variant: [default]
---
## Ejemplo
```tsx
const { canvasRef, renderFpsRef } = useAnimatedCanvas({
fps: 100,
draw: (ctx, w, h) => {
// Dibujar lo que sea a 100fps
ctx.fillStyle = '#3b82f6'
ctx.fillRect(0, 0, w * Math.random(), h)
},
})
return <canvas ref={canvasRef} style={{ width: '100%', height: 300 }} />
```
## Notas
- DPR automático: escala el canvas al devicePixelRatio de la pantalla
- Resize automático: detecta cambios de tamaño via getBoundingClientRect
- Throttle configurable: rAF corre a ~144fps nativo, el hook filtra a N fps
- FPS real: `renderFpsRef.current` tiene los FPS medidos (no el target)
- drawRef pattern: actualiza el callback sin re-crear el loop de animación
@@ -0,0 +1,80 @@
import { useEffect, useRef } from 'react'
export interface AnimatedCanvasOptions {
/** Target FPS (default 60). El throttle real es 1000/fps ms. */
fps?: number
/** Callback de dibujo. Recibe el canvas 2d context y dimensiones. */
draw: (ctx: CanvasRenderingContext2D, width: number, height: number, frameCount: number) => void
}
export interface AnimatedCanvasResult {
canvasRef: React.RefObject<HTMLCanvasElement | null>
/** FPS real de renderizado (actualizado cada segundo) */
renderFpsRef: React.RefObject<number>
}
/**
* Hook para renderizar un canvas a N fps usando requestAnimationFrame.
* Maneja DPR, resize, y throttling automático.
*/
export function useAnimatedCanvas(options: AnimatedCanvasOptions): AnimatedCanvasResult {
const { fps = 60, draw } = options
const canvasRef = useRef<HTMLCanvasElement>(null)
const renderFpsRef = useRef(0)
const rafRef = useRef(0)
const lastDrawRef = useRef(0)
const frameCountRef = useRef(0)
const fpsTimerRef = useRef(0)
const drawRef = useRef(draw)
// Keep draw ref updated without re-subscribing the effect
drawRef.current = draw
useEffect(() => {
const interval = 1000 / fps
const loop = (now: number) => {
rafRef.current = requestAnimationFrame(loop)
if (now - lastDrawRef.current < interval) return
lastDrawRef.current = now
// FPS counting
frameCountRef.current++
if (now - fpsTimerRef.current >= 1000) {
renderFpsRef.current = frameCountRef.current
frameCountRef.current = 0
fpsTimerRef.current = now
}
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const w = rect.width
const h = rect.height
const dpr = window.devicePixelRatio || 1
const targetW = Math.floor(w * dpr)
const targetH = Math.floor(h * dpr)
if (canvas.width !== targetW || canvas.height !== targetH) {
canvas.width = targetW
canvas.height = targetH
}
const ctx = canvas.getContext('2d')
if (!ctx) return
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
ctx.clearRect(0, 0, w, h)
drawRef.current(ctx, w, h, frameCountRef.current)
}
rafRef.current = requestAnimationFrame(loop)
return () => cancelAnimationFrame(rafRef.current)
}, [fps])
return { canvasRef, renderFpsRef }
}
+62
View File
@@ -0,0 +1,62 @@
---
name: use_wails_event
kind: component
lang: typescript
domain: ui
version: "1.0.0"
purity: impure
signature: "useWailsEvent<T>(opts: UseWailsEventOptions<T>): UseWailsEventResult<T>"
description: "Hook para suscripción a eventos Go→TS y emisión TS→Go via Wails runtime. Soporta once, maxCallbacks, emit bidireccional."
tags: [wails, event, hook, ipc, realtime, component, ui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/use_wails_event.tsx"
props:
- name: eventName
type: "string"
required: true
description: "Nombre del evento Wails"
- name: onEvent
type: "(data: T) => void"
required: false
description: "Callback al recibir evento"
- name: once
type: "boolean"
required: false
description: "Solo escuchar una vez"
- name: enabled
type: "boolean"
required: false
description: "Habilitar suscripción"
emits: [onEvent]
has_state: true
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/wails/use-wails-event.tsx"
---
## Ejemplo
```tsx
// Escuchar eventos de Go
const { lastData, eventCount, emit } = useWailsEvent<PriceUpdate>({
eventName: 'price:update',
onEvent: (price) => updateChart(price),
})
// Emitir de TS a Go
emit({ symbol: 'BTC', action: 'subscribe' })
```
## Notas
Exporta también `useWailsEmit()` para emit sin suscripción. Usa `window.runtime.EventsOn/Off/Emit` del Wails runtime.
+115
View File
@@ -0,0 +1,115 @@
import { useCallback, useEffect, useRef, useState } from 'react'
// Types for Wails runtime (will be available when running in Wails)
declare global {
interface Window {
runtime?: {
EventsOn: (eventName: string, callback: (...args: unknown[]) => void) => () => void
EventsOff: (eventName: string, ...additionalEventNames: string[]) => void
EventsOnce: (eventName: string, callback: (...args: unknown[]) => void) => () => void
EventsOnMultiple: (eventName: string, callback: (...args: unknown[]) => void, maxCallbacks: number) => () => void
EventsEmit: (eventName: string, ...data: unknown[]) => void
}
}
}
export interface UseWailsEventOptions<T> {
/** Nombre del evento a escuchar */
eventName: string
/** Callback cuando llega el evento */
onEvent?: (data: T) => void
/** Si está habilitado */
enabled?: boolean
/** Solo escuchar una vez */
once?: boolean
/** Número máximo de veces a escuchar */
maxCallbacks?: number
}
export interface UseWailsEventResult<T> {
/** Último dato recibido */
lastData: T | undefined
/** Número de eventos recibidos */
eventCount: number
/** Emitir un evento */
emit: (data?: T) => void
/** Resetear estado */
reset: () => void
}
/**
* Hook para suscribirse a eventos de Wails
*/
export function useWailsEvent<T = unknown>({
eventName,
onEvent,
enabled = true,
once = false,
maxCallbacks,
}: UseWailsEventOptions<T>): UseWailsEventResult<T> {
const [lastData, setLastData] = useState<T | undefined>(undefined)
const [eventCount, setEventCount] = useState(0)
const callbackRef = useRef(onEvent)
// Keep callback ref updated
useEffect(() => {
callbackRef.current = onEvent
}, [onEvent])
useEffect(() => {
if (!enabled || !window.runtime) return
const handleEvent = (...args: unknown[]) => {
const data = args[0] as T
setLastData(data)
setEventCount((c) => c + 1)
callbackRef.current?.(data)
}
let unsubscribe: (() => void) | undefined
if (once) {
unsubscribe = window.runtime.EventsOnce(eventName, handleEvent)
} else if (maxCallbacks !== undefined) {
unsubscribe = window.runtime.EventsOnMultiple(eventName, handleEvent, maxCallbacks)
} else {
unsubscribe = window.runtime.EventsOn(eventName, handleEvent)
}
return () => {
unsubscribe?.()
}
}, [eventName, enabled, once, maxCallbacks])
const emit = useCallback(
(data?: T) => {
if (window.runtime) {
window.runtime.EventsEmit(eventName, data)
}
},
[eventName]
)
const reset = useCallback(() => {
setLastData(undefined)
setEventCount(0)
}, [])
return {
lastData,
eventCount,
emit,
reset,
}
}
/**
* Hook para emitir eventos a Wails (sin suscripción)
*/
export function useWailsEmit() {
return useCallback((eventName: string, ...data: unknown[]) => {
if (window.runtime) {
window.runtime.EventsEmit(eventName, ...data)
}
}, [])
}
@@ -0,0 +1,64 @@
---
name: use_wails_mutation
kind: component
lang: typescript
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]
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/use_wails_mutation.tsx"
props:
- name: mutationFn
type: "(variables: TVariables) => Promise<TData>"
required: true
description: "Función que ejecuta la mutación via IPC"
- name: invalidateQueries
type: "string[][]"
required: false
description: "Query keys a invalidar en éxito"
- name: onMutate
type: "(variables: TVariables) => unknown"
required: false
description: "Optimistic update antes de la mutación"
emits: [onSuccess, onError, onSettled]
has_state: true
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/wails/use-wails-mutation.tsx"
---
## Ejemplo
```tsx
const { mutate, isLoading } = useWailsMutation({
mutationFn: (user: User) => CreateUser(user),
invalidateQueries: [['users']],
onMutate: (user) => {
// Optimistic: añadir al cache antes de confirmar
const prev = wailsCache.get<User[]>(['users', 'list'])
wailsCache.set(['users', 'list'], [...(prev || []), user])
return { prev }
},
onError: (_err, _vars, context) => {
// Rollback
wailsCache.set(['users', 'list'], context.prev)
},
})
```
## Notas
`mutate()` es fire-and-forget, `mutateAsync()` retorna Promise. Estado: idle → loading → success/error.
@@ -0,0 +1,150 @@
import { useCallback, useRef, useState } from 'react'
import { useWailsContext } from './wails_provider'
import type { MutationOptions } from '../../types/ui/wails_ipc'
export interface UseWailsMutationOptions<TData, TVariables> extends MutationOptions {
/** Función que ejecuta la mutación */
mutationFn: (variables: TVariables) => Promise<TData>
/** Callback antes de la mutación (optimistic update) */
onMutate?: (variables: TVariables) => Promise<unknown> | unknown
/** Callback en éxito */
onSuccess?: (data: TData, variables: TVariables, context: unknown) => void
/** Callback en error */
onError?: (error: Error, variables: TVariables, context: unknown) => void
/** Callback siempre (éxito o error) */
onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables, context: unknown) => void
/** Query keys a invalidar en éxito */
invalidateQueries?: string[][]
}
export interface UseWailsMutationResult<TData, TVariables> {
/** Ejecutar la mutación */
mutate: (variables: TVariables) => void
/** Ejecutar la mutación (async) */
mutateAsync: (variables: TVariables) => Promise<TData>
/** Estado de carga */
isLoading: boolean
/** Datos del resultado */
data: TData | undefined
/** Error si ocurrió */
error: Error | null
/** Resetear estado */
reset: () => void
/** Si fue exitoso */
isSuccess: boolean
/** Si hubo error */
isError: boolean
/** Si está idle */
isIdle: boolean
}
export function useWailsMutation<TData, TVariables = void>({
mutationFn,
onMutate,
onSuccess,
onError,
onSettled,
invalidateQueries,
retry = false,
retryDelay = 1000,
}: UseWailsMutationOptions<TData, TVariables>): UseWailsMutationResult<TData, TVariables> {
const { cache } = useWailsContext()
const [state, setState] = useState<{
status: 'idle' | 'loading' | 'success' | 'error'
data: TData | undefined
error: Error | null
}>({
status: 'idle',
data: undefined,
error: null,
})
const retryCountRef = useRef(0)
const executeMutation = useCallback(
async (variables: TVariables): Promise<TData> => {
setState((s) => ({ ...s, status: 'loading', error: null }))
let context: unknown
try {
// Optimistic update
if (onMutate) {
context = await onMutate(variables)
}
const data = await mutationFn(variables)
// Reset retry count on success
retryCountRef.current = 0
setState({
status: 'success',
data,
error: null,
})
// Invalidate queries
if (invalidateQueries) {
invalidateQueries.forEach((queryKey) => {
cache.invalidate(queryKey)
})
}
onSuccess?.(data, variables, context)
onSettled?.(data, null, variables, context)
return data
} catch (error) {
const maxRetries = typeof retry === 'number' ? retry : retry ? 3 : 0
if (retryCountRef.current < maxRetries) {
retryCountRef.current += 1
await new Promise((resolve) => setTimeout(resolve, retryDelay))
return executeMutation(variables)
}
setState((s) => ({
...s,
status: 'error',
error: error as Error,
}))
onError?.(error as Error, variables, context)
onSettled?.(undefined, error as Error, variables, context)
throw error
}
},
[mutationFn, onMutate, onSuccess, onError, onSettled, invalidateQueries, cache, retry, retryDelay]
)
const mutate = useCallback(
(variables: TVariables) => {
executeMutation(variables).catch(() => {})
},
[executeMutation]
)
const reset = useCallback(() => {
setState({
status: 'idle',
data: undefined,
error: null,
})
retryCountRef.current = 0
}, [])
return {
mutate,
mutateAsync: executeMutation,
isLoading: state.status === 'loading',
data: state.data,
error: state.error,
reset,
isSuccess: state.status === 'success',
isError: state.status === 'error',
isIdle: state.status === 'idle',
}
}
+65
View File
@@ -0,0 +1,65 @@
---
name: use_wails_query
kind: component
lang: typescript
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]
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/use_wails_query.tsx"
props:
- name: queryKey
type: "string[]"
required: true
description: "Key única para cache"
- name: queryFn
type: "() => Promise<T>"
required: true
description: "Función que llama al binding Wails"
- name: enabled
type: "boolean"
required: false
description: "Habilitar auto-fetch (default true)"
- name: refetchInterval
type: "number"
required: false
description: "Refetch cada N ms"
- name: staleTime
type: "number"
required: false
description: "Tiempo antes de considerar datos stale"
emits: [onSuccess, onError]
has_state: true
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/wails/use-wails-query.tsx"
---
## Ejemplo
```tsx
const { data, loading, refetch, invalidate } = useWailsQuery({
queryKey: ['users', 'list'],
queryFn: () => GetUsers(), // Wails binding
refetchInterval: 30000, // Cada 30s
staleTime: 5000, // Fresh por 5s
onSuccess: (users) => console.log(`${users.length} users`),
})
```
## Notas
API inspirada en TanStack Query pero sobre IPC Wails (sin HTTP). Cache compartido via WailsProvider o singleton. Refetch automático en window focus.
+163
View File
@@ -0,0 +1,163 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useWailsContext } from './wails_provider'
import { DEFAULT_QUERY_OPTIONS, type QueryOptions, type QueryState } from '../../types/ui/wails_ipc'
export interface UseWailsQueryOptions<T> extends QueryOptions {
/** Key única para identificar esta query en el cache */
queryKey: string[]
/** Función que ejecuta la llamada a Wails */
queryFn: () => Promise<T>
/** Callback en éxito */
onSuccess?: (data: T) => void
/** Callback en error */
onError?: (error: Error) => void
}
export interface UseWailsQueryResult<T> extends QueryState<T> {
/** Re-ejecutar la query manualmente */
refetch: () => Promise<T>
/** Invalidar cache de esta query */
invalidate: () => void
}
export function useWailsQuery<T>({
queryKey,
queryFn,
onSuccess,
onError,
...options
}: UseWailsQueryOptions<T>): UseWailsQueryResult<T> {
const { cache, defaultQueryOptions } = useWailsContext()
// Merge options with defaults
const opts = {
...DEFAULT_QUERY_OPTIONS,
...defaultQueryOptions,
...options,
}
const {
enabled,
staleTime,
refetchInterval,
refetchOnFocus,
retry,
retryDelay,
} = opts
const [state, setState] = useState<QueryState<T>>(() => ({
data: cache.get<T>(queryKey),
loading: !cache.has(queryKey) && enabled,
error: null,
isStale: cache.isStale(queryKey, staleTime),
lastUpdated: cache.getTimestamp(queryKey),
}))
const retryCountRef = useRef(0)
const mountedRef = useRef(true)
const fetch = useCallback(async (): Promise<T> => {
if (!mountedRef.current) throw new Error('Component unmounted')
setState((s) => ({ ...s, loading: true, error: null }))
try {
const data = await queryFn()
if (!mountedRef.current) throw new Error('Component unmounted')
cache.set(queryKey, data)
retryCountRef.current = 0
setState({
data,
loading: false,
error: null,
isStale: false,
lastUpdated: new Date(),
})
onSuccess?.(data)
return data
} catch (error) {
if (!mountedRef.current) throw error
const maxRetries = typeof retry === 'number' ? retry : retry ? 3 : 0
if (retryCountRef.current < maxRetries) {
retryCountRef.current += 1
await new Promise((resolve) => setTimeout(resolve, retryDelay))
return fetch()
}
setState((s) => ({
...s,
loading: false,
error: error as Error,
}))
onError?.(error as Error)
throw error
}
}, [queryKey.join(':'), queryFn, cache, staleTime, retry, retryDelay, onSuccess, onError])
// Initial fetch
useEffect(() => {
mountedRef.current = true
if (enabled && (!cache.has(queryKey) || cache.isStale(queryKey, staleTime))) {
fetch().catch(() => {})
}
return () => {
mountedRef.current = false
}
}, [enabled, queryKey.join(':'), staleTime])
// Refetch interval
useEffect(() => {
if (!refetchInterval || !enabled) return
const interval = setInterval(() => {
fetch().catch(() => {})
}, refetchInterval)
return () => clearInterval(interval)
}, [refetchInterval, enabled, fetch])
// Refetch on window focus
useEffect(() => {
if (!refetchOnFocus || !enabled) return
const handleFocus = () => {
if (cache.isStale(queryKey, staleTime)) {
fetch().catch(() => {})
}
}
window.addEventListener('focus', handleFocus)
return () => window.removeEventListener('focus', handleFocus)
}, [refetchOnFocus, enabled, queryKey.join(':'), staleTime, fetch])
// Subscribe to cache changes
useEffect(() => {
return cache.subscribe(queryKey, () => {
setState((s) => ({
...s,
data: cache.get<T>(queryKey),
isStale: cache.isStale(queryKey, staleTime),
lastUpdated: cache.getTimestamp(queryKey),
}))
})
}, [queryKey.join(':'), staleTime])
const invalidate = useCallback(() => {
cache.invalidate(queryKey)
}, [queryKey.join(':')])
return {
...state,
refetch: fetch,
invalidate,
}
}
+69
View File
@@ -0,0 +1,69 @@
---
name: use_wails_stream
kind: component
lang: typescript
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_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/use_wails_stream.tsx"
props:
- name: streamName
type: "string"
required: true
description: "Nombre del evento de stream"
- name: startFn
type: "() => Promise<void>"
required: false
description: "Función Go para iniciar el stream"
- name: stopFn
type: "() => Promise<void>"
required: false
description: "Función Go para detener el stream"
- name: bufferSize
type: "number"
required: false
description: "Tamaño máximo del buffer (default 1000)"
- name: transform
type: "(data: unknown) => T"
required: false
description: "Transformar datos entrantes"
emits: [onData, onComplete, onError]
has_state: true
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/wails/use-wails-stream.tsx"
---
## Ejemplo
```tsx
// Stream de logs en tiempo real
const { data: logs, isStreaming, start, stop, clear } = useWailsStream<string>({
streamName: 'app:logs',
startFn: () => StartLogStream(),
stopFn: () => StopLogStream(),
bufferSize: 500,
autoStart: true,
})
// Versión simplificada para logs
const { data: logs } = useWailsLogs('deploy:logs')
```
## Notas
Protocolo de stream: Go emite chunks en `{streamName}`, completa con `{streamName}:complete`, errores con `{streamName}:error`. Buffer circular con tamaño configurable.
+196
View File
@@ -0,0 +1,196 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useWailsEvent } from './use_wails_event'
export interface UseWailsStreamOptions<T> {
/** Nombre del evento de stream */
streamName: string
/** Función para iniciar el stream en Go */
startFn?: () => Promise<void>
/** Función para detener el stream en Go */
stopFn?: () => Promise<void>
/** Transformar datos entrantes */
transform?: (data: unknown) => T
/** Callback por cada chunk */
onData?: (data: T) => void
/** Callback cuando el stream termina */
onComplete?: () => void
/** Callback en error */
onError?: (error: Error) => void
/** Tamaño máximo del buffer */
bufferSize?: number
/** Auto-iniciar el stream */
autoStart?: boolean
}
export interface UseWailsStreamResult<T> {
/** Datos acumulados */
data: T[]
/** Último dato recibido */
lastChunk: T | undefined
/** Si está activo */
isStreaming: boolean
/** Si está cargando (iniciando) */
isLoading: boolean
/** Error si ocurrió */
error: Error | null
/** Iniciar stream */
start: () => Promise<void>
/** Detener stream */
stop: () => Promise<void>
/** Limpiar buffer */
clear: () => void
/** Número de chunks recibidos */
chunkCount: number
}
/**
* Hook para manejar streams de datos desde Wails
* Útil para datos en tiempo real como logs, métricas, etc.
*/
export function useWailsStream<T = unknown>({
streamName,
startFn,
stopFn,
transform,
onData,
onComplete,
onError,
bufferSize = 1000,
autoStart = false,
}: UseWailsStreamOptions<T>): UseWailsStreamResult<T> {
const [data, setData] = useState<T[]>([])
const [isStreaming, setIsStreaming] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [chunkCount, setChunkCount] = useState(0)
const lastChunkRef = useRef<T | undefined>(undefined)
const mountedRef = useRef(true)
// Listen for stream data
useWailsEvent<unknown>({
eventName: streamName,
enabled: isStreaming,
onEvent: (rawData) => {
if (!mountedRef.current) return
const chunk = transform ? transform(rawData) : (rawData as T)
lastChunkRef.current = chunk
setData((prev) => {
const newData = [...prev, chunk]
// Mantener buffer limitado
if (newData.length > bufferSize) {
return newData.slice(-bufferSize)
}
return newData
})
setChunkCount((c) => c + 1)
onData?.(chunk)
},
})
// Listen for stream complete
useWailsEvent({
eventName: `${streamName}:complete`,
enabled: isStreaming,
onEvent: () => {
if (!mountedRef.current) return
setIsStreaming(false)
onComplete?.()
},
})
// Listen for stream error
useWailsEvent<{ message: string }>({
eventName: `${streamName}:error`,
enabled: isStreaming,
onEvent: (errorData) => {
if (!mountedRef.current) return
const err = new Error(errorData?.message || 'Stream error')
setError(err)
setIsStreaming(false)
onError?.(err)
},
})
const start = useCallback(async () => {
if (isStreaming || isLoading) return
setIsLoading(true)
setError(null)
try {
if (startFn) {
await startFn()
}
setIsStreaming(true)
} catch (err) {
const error = err as Error
setError(error)
onError?.(error)
} finally {
setIsLoading(false)
}
}, [isStreaming, isLoading, startFn, onError])
const stop = useCallback(async () => {
if (!isStreaming) return
try {
if (stopFn) {
await stopFn()
}
setIsStreaming(false)
} catch (err) {
const error = err as Error
setError(error)
onError?.(error)
}
}, [isStreaming, stopFn, onError])
const clear = useCallback(() => {
setData([])
setChunkCount(0)
lastChunkRef.current = undefined
}, [])
// Auto-start
useEffect(() => {
if (autoStart) {
start()
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps
// Cleanup
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
}
}, [])
return {
data,
lastChunk: lastChunkRef.current,
isStreaming,
isLoading,
error,
start,
stop,
clear,
chunkCount,
}
}
/**
* Hook simplificado para logs en tiempo real
*/
export function useWailsLogs(eventName: string = 'logs') {
return useWailsStream<string>({
streamName: eventName,
transform: (data) => (typeof data === 'string' ? data : JSON.stringify(data)),
bufferSize: 500,
})
}
+53
View File
@@ -0,0 +1,53 @@
---
name: wails_provider
kind: component
lang: typescript
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]
returns: []
returns_optional: false
error_type: ""
imports: [react]
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/wails_provider.tsx"
props:
- name: cache
type: "WailsCache"
required: false
description: "Cache custom (default: singleton global)"
- name: defaultQueryOptions
type: "Partial<QueryOptions>"
required: false
description: "Opciones default para todas las queries"
- name: children
type: "ReactNode"
required: true
description: "App content"
emits: []
has_state: false
framework: react
variant: [default]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/wails/wails-provider.tsx"
---
## Ejemplo
```tsx
<WailsProvider defaultQueryOptions={{ staleTime: 5000, retry: 2 }}>
<App />
</WailsProvider>
```
## Notas
Sin provider, los hooks hacen fallback al singleton `wailsCache`. El provider solo es necesario para cache custom o opciones globales.
+60
View File
@@ -0,0 +1,60 @@
import { createContext, useContext, useMemo, type ReactNode } from 'react'
import { WailsCache, wailsCache } from '../core/wails_cache'
import type { QueryOptions, MutationOptions } from '../../types/ui/wails_ipc'
interface WailsContextValue {
cache: WailsCache
defaultQueryOptions: Partial<QueryOptions>
defaultMutationOptions: Partial<MutationOptions>
}
const WailsContext = createContext<WailsContextValue | null>(null)
export interface WailsProviderProps {
children: ReactNode
/** Usar un cache custom en lugar del singleton */
cache?: WailsCache
/** Opciones por defecto para queries */
defaultQueryOptions?: Partial<QueryOptions>
/** Opciones por defecto para mutations */
defaultMutationOptions?: Partial<MutationOptions>
}
export function WailsProvider({
children,
cache,
defaultQueryOptions = {},
defaultMutationOptions = {},
}: WailsProviderProps) {
const value = useMemo<WailsContextValue>(
() => ({
cache: cache ?? wailsCache,
defaultQueryOptions,
defaultMutationOptions,
}),
[cache, defaultQueryOptions, defaultMutationOptions]
)
return (
<WailsContext.Provider value={value}>
{children}
</WailsContext.Provider>
)
}
export function useWailsContext(): WailsContextValue {
const context = useContext(WailsContext)
if (!context) {
// Fallback to singleton cache if no provider
return {
cache: wailsCache,
defaultQueryOptions: {},
defaultMutationOptions: {},
}
}
return context
}
export function useWailsCache(): WailsCache {
return useWailsContext().cache
}
+40
View File
@@ -0,0 +1,40 @@
---
name: ComponentVariants
lang: typescript
domain: core
version: "1.0.0"
algebraic: product
definition: |
interface ComponentBaseProps { className?: string; children?: React.ReactNode }
type PropsWithVariants<V> = ComponentBaseProps & VariantProps<V>
description: "Tipos base para componentes con variantes CVA. Props comunes y composición de variantes type-safe."
tags: [component, variants, cva, props, base]
uses_types: []
file_path: "frontend/types/core/component_variants.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/button.tsx"
---
## Tipos exportados
- `VariantProps<V>` — re-exportado de `class-variance-authority`. Extrae el tipo de props de un `cva()` call.
- `ComponentBaseProps` — props comunes a todos los componentes: `className` y `children` opcionales.
- `PropsWithVariants<V>` — combinación de `ComponentBaseProps` con las variantes de un `cva()` concreto. Usar como base para las props de cualquier componente con variantes.
## Uso
```typescript
import { cva } from "class-variance-authority"
import { type PropsWithVariants } from "@/types/core/component_variants"
const buttonVariants = cva("base-classes", {
variants: { size: { sm: "...", md: "..." } }
})
type ButtonProps = PropsWithVariants<typeof buttonVariants>
```
## Notas
Requiere `class-variance-authority` como dependencia del proyecto consumidor. `React.ReactNode` requiere que `react` esté en el scope — en proyectos con `@types/react` esto se resuelve automáticamente.
+10
View File
@@ -0,0 +1,10 @@
import { type VariantProps } from "class-variance-authority"
export type { VariantProps }
export interface ComponentBaseProps {
className?: string
children?: React.ReactNode
}
export type PropsWithVariants<V> = ComponentBaseProps & VariantProps<V>
+26
View File
@@ -0,0 +1,26 @@
---
name: ChartSeries
lang: typescript
domain: ui
version: "1.0.0"
algebraic: product
definition: |
interface Series { key: string; name: string; color?: string }
interface ChartDataPoint { [key: string]: string | number }
description: "Tipos base para series y datos de gráficos. Usados por todos los chart components."
tags: [chart, series, data, visualization]
uses_types: []
file_path: "frontend/types/ui/chart_series.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/charts/chart-base.tsx"
---
## Interfaces exportadas
- `Series` — describe una serie de datos: `key` (campo en el datapoint), `name` (label visible), `color` opcional (override del color del tema)
- `ChartDataPoint` — mapa genérico `string -> string | number`, representa un punto de datos indexado por clave
## Notas
Tipos mínimos sin dependencias. `ChartDataPoint` usa index signature para permitir cualquier combinación de campos numéricos y categóricos en el mismo punto de datos.
+9
View File
@@ -0,0 +1,9 @@
export interface Series {
key: string
name: string
color?: string
}
export interface ChartDataPoint {
[key: string]: string | number
}
+41
View File
@@ -0,0 +1,41 @@
---
name: ThemeConfig
lang: typescript
domain: ui
version: "1.0.0"
algebraic: product
definition: |
interface ThemeConfig { name: string; label: string; palette: ColorPalette; colors: SemanticColors; typography: Typography; spacing: Spacing; borders: Borders; shadows: Shadows; motion: Motion; breakpoints: Breakpoints; zIndex: ZIndex; icons: Icons }
description: "Sistema completo de tokens de diseño: colores semánticos, tipografía, spacing, sombras, motion, breakpoints. Base del theming de Frontend Library."
tags: [theme, design-tokens, colors, typography, ui]
uses_types: []
file_path: "frontend/types/ui/theme_config.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/themes/types.ts"
---
## Interfaces exportadas
- `ColorPalette` — colores primitivos (grays, brand, status, white/black)
- `SemanticColors` — tokens semánticos (background, foreground, border, brand, status, surface, sidebar, chart)
- `Typography` — fontFamily, fontSize, fontWeight, lineHeight, letterSpacing
- `Spacing` — escala 0-96 basada en 4px
- `Borders` — width y radius
- `Shadows` — elevación xs-2xl, inner, glow
- `Motion` — duration e easing
- `Breakpoints` — xs-2xl
- `ZIndex` — jerarquía de capas hide-max
- `Icons` — size y strokeWidth para Phosphor Icons
- `ThemeConfig` — composición completa de todos los tokens
- `ThemeColors` — formato legacy para compatibilidad con shadcn/ui
- `Theme` — nombre + label + ThemeColors
- `ThemeName` — union type de los temas disponibles
## Helper
`themeConfigToColors(config: ThemeConfig): ThemeColors` — convierte un ThemeConfig al formato legacy ThemeColors que consume shadcn/ui.
## Notas
Copia directa de `frontend/src/themes/types.ts` de Frontend_Library. El archivo es autocontenido sin dependencias externas. Los colores usan formato OKLCH via variables CSS.
+540
View File
@@ -0,0 +1,540 @@
// ============================================================================
// THEME SYSTEM TYPES
// Sistema de temas completo para Frontend Library
// ============================================================================
// ----------------------------------------------------------------------------
// 1. COLOR PALETTE (Primitivos)
// ----------------------------------------------------------------------------
export interface ColorPalette {
// Grays (escala neutral)
gray50: string
gray100: string
gray200: string
gray300: string
gray400: string
gray500: string
gray600: string
gray700: string
gray800: string
gray900: string
gray950: string
// Brand colors (personalizables por tema)
brand50: string
brand100: string
brand200: string
brand300: string
brand400: string
brand500: string
brand600: string
brand700: string
brand800: string
brand900: string
brand950: string
// Status colors
red500: string
red600: string
green500: string
green600: string
yellow500: string
yellow600: string
blue500: string
blue600: string
// Pure colors
white: string
black: string
transparent: string
}
// ----------------------------------------------------------------------------
// 2. SEMANTIC COLORS (Tokens semánticos)
// ----------------------------------------------------------------------------
export interface SemanticColors {
// Backgrounds
background: {
default: string
subtle: string
muted: string
inverse: string
}
// Foregrounds
foreground: {
default: string
muted: string
subtle: string
disabled: string
inverse: string
}
// Borders
border: {
default: string
muted: string
strong: string
focus: string
}
// Brand
brand: {
primary: string
primaryForeground: string
secondary: string
secondaryForeground: string
accent: string
accentForeground: string
}
// Status
status: {
success: string
successForeground: string
successMuted: string
warning: string
warningForeground: string
warningMuted: string
error: string
errorForeground: string
errorMuted: string
info: string
infoForeground: string
infoMuted: string
}
// Surfaces (jerarquía de capas)
surface: {
base: string // Fondo principal
raised: string // Cards, panels
overlay: string // Modals, dropdowns
sunken: string // Inputs, wells
}
// Sidebar (panel lateral oscuro)
sidebar: {
background: string
foreground: string
muted: string
accent: string
accentForeground: string
border: string
ring: string
}
// Charts
chart: {
1: string
2: string
3: string
4: string
5: string
6: string
7: string
8: string
}
// Interactive
ring: string
input: string
}
// ----------------------------------------------------------------------------
// 3. TYPOGRAPHY
// ----------------------------------------------------------------------------
export interface Typography {
fontFamily: {
display: string // Títulos destacados
heading: string // Encabezados
body: string // Texto general
mono: string // Código
}
fontSize: {
xs: string // 12px
sm: string // 14px
base: string // 16px
lg: string // 18px
xl: string // 20px
'2xl': string // 24px
'3xl': string // 30px
'4xl': string // 36px
'5xl': string // 48px
'6xl': string // 60px
}
fontWeight: {
light: number
normal: number
medium: number
semibold: number
bold: number
extrabold: number
}
lineHeight: {
none: number
tight: number
snug: number
normal: number
relaxed: number
loose: number
}
letterSpacing: {
tighter: string
tight: string
normal: string
wide: string
wider: string
}
}
// ----------------------------------------------------------------------------
// 4. SPACING
// ----------------------------------------------------------------------------
export interface Spacing {
0: string
px: string
0.5: string
1: string
1.5: string
2: string
2.5: string
3: string
3.5: string
4: string
5: string
6: string
7: string
8: string
9: string
10: string
11: string
12: string
14: string
16: string
20: string
24: string
28: string
32: string
36: string
40: string
44: string
48: string
52: string
56: string
60: string
64: string
72: string
80: string
96: string
}
// ----------------------------------------------------------------------------
// 5. BORDERS
// ----------------------------------------------------------------------------
export interface Borders {
width: {
none: string
thin: string
default: string
medium: string
thick: string
}
radius: {
none: string
sm: string
default: string
md: string
lg: string
xl: string
'2xl': string
'3xl': string
full: string
}
}
// ----------------------------------------------------------------------------
// 6. SHADOWS (Elevation)
// ----------------------------------------------------------------------------
export interface Shadows {
none: string
xs: string
sm: string
default: string
md: string
lg: string
xl: string
'2xl': string
inner: string
// Colored shadows for glow effects
glow: {
sm: string
md: string
lg: string
}
}
// ----------------------------------------------------------------------------
// 7. MOTION / ANIMATION
// ----------------------------------------------------------------------------
export interface Motion {
duration: {
instant: string // 0ms
fast: string // 100ms
normal: string // 200ms
slow: string // 300ms
slower: string // 500ms
slowest: string // 1000ms
}
easing: {
linear: string
default: string
in: string
out: string
inOut: string
spring: string
bounce: string
}
}
// ----------------------------------------------------------------------------
// 8. BREAKPOINTS
// ----------------------------------------------------------------------------
export interface Breakpoints {
xs: string
sm: string
md: string
lg: string
xl: string
'2xl': string
}
// ----------------------------------------------------------------------------
// 9. Z-INDEX
// ----------------------------------------------------------------------------
export interface ZIndex {
hide: number
base: number
raised: number
dropdown: number
sticky: number
overlay: number
modal: number
popover: number
toast: number
tooltip: number
max: number
}
// ----------------------------------------------------------------------------
// 10. ICONS
// ----------------------------------------------------------------------------
export interface Icons {
size: {
xs: string // 12px
sm: string // 16px
default: string // 20px
md: string // 24px
lg: string // 32px
xl: string // 40px
'2xl': string // 48px
}
strokeWidth: {
thin: number
light: number
regular: number
bold: number
fill: number
}
}
// ----------------------------------------------------------------------------
// THEME CONFIGURATION (Complete)
// ----------------------------------------------------------------------------
export interface ThemeConfig {
// Metadata
name: string
label: string
description?: string
// Color system
palette: ColorPalette
colors: SemanticColors
// Typography
typography: Typography
// Layout
spacing: Spacing
borders: Borders
shadows: Shadows
// Interaction
motion: Motion
// Responsive
breakpoints: Breakpoints
zIndex: ZIndex
// Icons
icons: Icons
}
// ----------------------------------------------------------------------------
// LEGACY SUPPORT - ThemeColors (for backward compatibility)
// ----------------------------------------------------------------------------
export interface ThemeColors {
// Base
background: string
foreground: string
// Cards & Popovers
card: string
cardForeground: string
popover: string
popoverForeground: string
// Primary & Secondary
primary: string
primaryForeground: string
secondary: string
secondaryForeground: string
// Muted & Accent
muted: string
mutedForeground: string
accent: string
accentForeground: string
// Semantic colors
destructive: string
destructiveForeground: string
success: string
successForeground: string
warning: string
warningForeground: string
info: string
infoForeground: string
// Surfaces
surface: string
surfaceHover: string
overlay: string
// Form elements
border: string
input: string
ring: string
// Charts
chart1: string
chart2: string
chart3: string
chart4: string
chart5: string
// Sidebar
sidebar: string
sidebarForeground: string
sidebarPrimary: string
sidebarPrimaryForeground: string
sidebarAccent: string
sidebarAccentForeground: string
sidebarBorder: string
sidebarRing: string
}
export interface Theme {
name: string
label: string
colors: ThemeColors
}
export type ThemeName = 'default' | 'dark' | 'blue' | 'green' | 'purple' | 'orange' | 'rose' | 'cyan' | 'sunset' | 'midnight'
// ----------------------------------------------------------------------------
// HELPER: Convert ThemeConfig to legacy ThemeColors
// ----------------------------------------------------------------------------
export function themeConfigToColors(config: ThemeConfig): ThemeColors {
const { colors } = config
return {
// Base
background: colors.background.default,
foreground: colors.foreground.default,
// Cards & Popovers
card: colors.surface.raised,
cardForeground: colors.foreground.default,
popover: colors.surface.overlay,
popoverForeground: colors.foreground.default,
// Primary & Secondary
primary: colors.brand.primary,
primaryForeground: colors.brand.primaryForeground,
secondary: colors.brand.secondary,
secondaryForeground: colors.brand.secondaryForeground,
// Muted & Accent
muted: colors.background.muted,
mutedForeground: colors.foreground.muted,
accent: colors.brand.accent,
accentForeground: colors.brand.accentForeground,
// Semantic colors
destructive: colors.status.error,
destructiveForeground: colors.status.errorForeground,
success: colors.status.success,
successForeground: colors.status.successForeground,
warning: colors.status.warning,
warningForeground: colors.status.warningForeground,
info: colors.status.info,
infoForeground: colors.status.infoForeground,
// Surfaces
surface: colors.surface.raised,
surfaceHover: colors.background.subtle,
overlay: colors.surface.overlay,
// Form elements
border: colors.border.default,
input: colors.border.default,
ring: colors.ring,
// Charts
chart1: colors.chart[1],
chart2: colors.chart[2],
chart3: colors.chart[3],
chart4: colors.chart[4],
chart5: colors.chart[5],
// Sidebar
sidebar: colors.sidebar.background,
sidebarForeground: colors.sidebar.foreground,
sidebarPrimary: colors.brand.primary,
sidebarPrimaryForeground: colors.brand.primaryForeground,
sidebarAccent: colors.sidebar.accent,
sidebarAccentForeground: colors.sidebar.accentForeground,
sidebarBorder: colors.sidebar.border,
sidebarRing: colors.sidebar.ring,
}
}
+22
View File
@@ -0,0 +1,22 @@
---
name: WailsIPC
lang: typescript
domain: ui
version: "1.0.0"
algebraic: product
definition: |
interface QueryState<T> { data: T | null; loading: boolean; error: Error | null; isStale: boolean; lastUpdated: Date | null }
interface QueryOptions { enabled?: boolean; refetchInterval?: number; refetchOnFocus?: boolean; staleTime?: number; cacheTime?: number; retry?: number | boolean; retryDelay?: number }
interface MutationOptions { retry?: number | boolean; retryDelay?: number }
description: "Tipos base para el sistema IPC de Wails: QueryState, QueryOptions, MutationOptions, WailsEvent, defaults."
tags: [wails, ipc, types, query, mutation]
uses_types: []
file_path: "frontend/types/ui/wails_ipc.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/wails/types.ts"
---
## Notas
Incluye constantes DEFAULT_QUERY_OPTIONS y DEFAULT_MUTATION_OPTIONS con valores sensibles para IPC (staleTime: 0, cacheTime: 5min, retry: 3).
+64
View File
@@ -0,0 +1,64 @@
/** Estado de una query */
export interface QueryState<T> {
data: T | null
loading: boolean
error: Error | null
isStale: boolean
lastUpdated: Date | null
}
/** Opciones comunes para queries */
export interface QueryOptions {
/** Ejecutar automáticamente (default: true) */
enabled?: boolean
/** Auto-refetch cada N ms */
refetchInterval?: number
/** Refetch cuando la ventana gana foco */
refetchOnFocus?: boolean
/** Tiempo en ms antes de considerar datos stale (default: 0) */
staleTime?: number
/** Tiempo en ms en cache después de unmount (default: 5 min) */
cacheTime?: number
/** Número de reintentos en error (default: 3) */
retry?: number | boolean
/** Delay entre reintentos en ms */
retryDelay?: number
}
/** Opciones para mutations */
export interface MutationOptions {
/** Reintentos en error */
retry?: number | boolean
/** Delay entre reintentos */
retryDelay?: number
}
/** Resultado de un evento Wails */
export interface WailsEvent<T> {
name: string
data: T
timestamp: Date
}
/** Estado de una mutation */
export interface MutationState<T> {
data: T | null
loading: boolean
error: Error | null
}
/** Default options */
export const DEFAULT_QUERY_OPTIONS: Required<QueryOptions> = {
enabled: true,
refetchInterval: 0,
refetchOnFocus: true,
staleTime: 0,
cacheTime: 5 * 60 * 1000, // 5 minutos
retry: 3,
retryDelay: 1000,
}
export const DEFAULT_MUTATION_OPTIONS: Required<MutationOptions> = {
retry: 0,
retryDelay: 1000,
}