diff --git a/frontend/functions/core/chart_colors.md b/frontend/functions/core/chart_colors.md new file mode 100644 index 00000000..8bb9bd2a --- /dev/null +++ b/frontend/functions/core/chart_colors.md @@ -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. diff --git a/frontend/functions/core/chart_colors.ts b/frontend/functions/core/chart_colors.ts new file mode 100644 index 00000000..fc55e8cf --- /dev/null +++ b/frontend/functions/core/chart_colors.ts @@ -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] +} diff --git a/frontend/functions/core/cn.md b/frontend/functions/core/cn.md new file mode 100644 index 00000000..288ae534 --- /dev/null +++ b/frontend/functions/core/cn.md @@ -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. diff --git a/frontend/functions/core/cn.ts b/frontend/functions/core/cn.ts new file mode 100644 index 00000000..e3155433 --- /dev/null +++ b/frontend/functions/core/cn.ts @@ -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)) +} diff --git a/frontend/functions/core/format_compact.md b/frontend/functions/core/format_compact.md new file mode 100644 index 00000000..6542fa5b --- /dev/null +++ b/frontend/functions/core/format_compact.md @@ -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. diff --git a/frontend/functions/core/format_compact.ts b/frontend/functions/core/format_compact.ts new file mode 100644 index 00000000..ff7e4b9b --- /dev/null +++ b/frontend/functions/core/format_compact.ts @@ -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' +} diff --git a/frontend/functions/core/get_series_color.md b/frontend/functions/core/get_series_color.md new file mode 100644 index 00000000..6bb5aa95 --- /dev/null +++ b/frontend/functions/core/get_series_color.md @@ -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. diff --git a/frontend/functions/core/get_series_color.ts b/frontend/functions/core/get_series_color.ts new file mode 100644 index 00000000..0c89b11a --- /dev/null +++ b/frontend/functions/core/get_series_color.ts @@ -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 } diff --git a/frontend/functions/core/theme_config_to_colors.md b/frontend/functions/core/theme_config_to_colors.md new file mode 100644 index 00000000..b3a7dab0 --- /dev/null +++ b/frontend/functions/core/theme_config_to_colors.md @@ -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). diff --git a/frontend/functions/core/theme_config_to_colors.ts b/frontend/functions/core/theme_config_to_colors.ts new file mode 100644 index 00000000..884155db --- /dev/null +++ b/frontend/functions/core/theme_config_to_colors.ts @@ -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, + } +} diff --git a/frontend/functions/core/wails_cache.md b/frontend/functions/core/wails_cache.md new file mode 100644 index 00000000..2296276d --- /dev/null +++ b/frontend/functions/core/wails_cache.md @@ -0,0 +1,39 @@ +--- +name: wails_cache +kind: function +lang: typescript +domain: core +version: "1.0.0" +purity: pure +signature: "class WailsCache { get(key: string[]): T | null; set(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(['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. diff --git a/frontend/functions/core/wails_cache.ts b/frontend/functions/core/wails_cache.ts new file mode 100644 index 00000000..10f2c0ed --- /dev/null +++ b/frontend/functions/core/wails_cache.ts @@ -0,0 +1,99 @@ +interface CacheEntry { + data: unknown + timestamp: Date +} + +export class WailsCache { + private cache = new Map() + private subscribers = new Map void>>() + + /** Generar key string desde array */ + private getKey(queryKey: string[]): string { + return queryKey.join(':') + } + + /** Obtener dato del cache */ + get(queryKey: string[]): T | null { + const entry = this.cache.get(this.getKey(queryKey)) + return (entry?.data as T) ?? null + } + + /** Guardar dato en cache */ + set(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() diff --git a/frontend/functions/ui/alert.md b/frontend/functions/ui/alert.md new file mode 100644 index 00000000..fe735977 --- /dev/null +++ b/frontend/functions/ui/alert.md @@ -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 + + + Error + Something went wrong. + +``` + +## 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). diff --git a/frontend/functions/ui/alert.tsx b/frontend/functions/ui/alert.tsx new file mode 100644 index 00000000..788daf70 --- /dev/null +++ b/frontend/functions/ui/alert.tsx @@ -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) { + return
+} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return
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
+} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return
+} + +export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants } diff --git a/frontend/functions/ui/analytics_page.md b/frontend/functions/ui/analytics_page.md new file mode 100644 index 00000000..08846bd5 --- /dev/null +++ b/frontend/functions/ui/analytics_page.md @@ -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: }, + { id: 'orders', title: 'Orders by Category', type: 'bar', content: }, + { id: 'trends', title: 'Customer Trends', type: 'line', content: }, + ], +}) +``` + +## Notas + +Layout inteligente: los KPIs se ajustan automáticamente a 2/3/4 columnas según cantidad. Los charts soportan span para ancho completo. diff --git a/frontend/functions/ui/analytics_page.tsx b/frontend/functions/ui/analytics_page.tsx new file mode 100644 index 00000000..9208aa03 --- /dev/null +++ b/frontend/functions/ui/analytics_page.tsx @@ -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 ( +
+ {/* Header */} +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ {dateRange} + {actions} +
+
+ + {/* KPI Row */} +
+ {metrics.map((metric, i) => ( +
+

{metric.label}

+
+
+

{metric.value}

+ {metric.delta && ( +
+ {metric.delta.value > 0 ? '+' : ''}{metric.delta.value}% +
+ )} +
+
+
+ ))} +
+ + {/* Charts Grid */} +
+ {charts.map((chart) => ( +
+

{chart.title}

+ {chart.content} +
+ ))} +
+
+ ) +} + +export type { AnalyticsPageProps, MetricConfig, ChartConfig } diff --git a/frontend/functions/ui/apply_theme.md b/frontend/functions/ui/apply_theme.md new file mode 100644 index 00000000..9991facc --- /dev/null +++ b/frontend/functions/ui/apply_theme.md @@ -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. diff --git a/frontend/functions/ui/apply_theme.tsx b/frontend/functions/ui/apply_theme.tsx new file mode 100644 index 00000000..49eafbd5 --- /dev/null +++ b/frontend/functions/ui/apply_theme.tsx @@ -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 = { + 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 } diff --git a/frontend/functions/ui/area_chart.md b/frontend/functions/ui/area_chart.md new file mode 100644 index 00000000..92a95495 --- /dev/null +++ b/frontend/functions/ui/area_chart.md @@ -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[]" + required: true + description: "Array de datos" + - name: xKey + type: "string" + required: true + description: "Key del eje X" + - name: stacked + type: "boolean" + required: false + description: "Apilar áreas" + - name: gradient + type: "GradientConfig | boolean" + required: false + description: "Gradiente (true por defecto)" + - name: series + type: "Series[]" + required: false + description: "Series de datos para multi-series" +emits: [] +has_state: false +framework: react +variant: [default, stacked] +source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library" +source_license: "MIT" +source_file: "frontend/src/components/ui/charts/area-chart.tsx" +--- + +## Ejemplo + +```tsx + + +``` diff --git a/frontend/functions/ui/area_chart.tsx b/frontend/functions/ui/area_chart.tsx new file mode 100644 index 00000000..f0ef51cb --- /dev/null +++ b/frontend/functions/ui/area_chart.tsx @@ -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[] + 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 ( + + + + {areas.map((area) => ( + + + + + ))} + + {showGrid && } + + + } cursor={{ stroke: 'hsl(var(--muted-foreground))', strokeDasharray: '3 3' }} /> + {showLegend && } + {areas.map((area) => ( + + ))} + + + ) +} + +export const AreaChart = AreaChartComponent +export type { AreaChartProps, GradientConfig } diff --git a/frontend/functions/ui/badge.md b/frontend/functions/ui/badge.md new file mode 100644 index 00000000..4558d802 --- /dev/null +++ b/frontend/functions/ui/badge.md @@ -0,0 +1,48 @@ +--- +name: badge +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Badge(props: BadgeProps & VariantProps): JSX.Element" +description: "Badge con 10 variantes semánticas (default, secondary, destructive, outline, ghost, link, success, warning, error, info) y 2 tamaños." +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 +Active +Error +``` + +## Notas + +Versión simplificada que usa span nativo en lugar de useRender de Base-UI. Mantiene todas las variantes y la composibilidad con cn(). diff --git a/frontend/functions/ui/badge.tsx b/frontend/functions/ui/badge.tsx new file mode 100644 index 00000000..76f1fece --- /dev/null +++ b/frontend/functions/ui/badge.tsx @@ -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, VariantProps {} + +function Badge({ className, variant = "default", size = "default", ...props }: BadgeProps) { + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/functions/ui/bar_chart.md b/frontend/functions/ui/bar_chart.md new file mode 100644 index 00000000..0857994f --- /dev/null +++ b/frontend/functions/ui/bar_chart.md @@ -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[]" + 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 + + +``` diff --git a/frontend/functions/ui/bar_chart.tsx b/frontend/functions/ui/bar_chart.tsx new file mode 100644 index 00000000..2a37f122 --- /dev/null +++ b/frontend/functions/ui/bar_chart.tsx @@ -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[] + 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 ( + + + {showGrid && } + {horizontal ? ( + <> + + + + ) : ( + <> + + + + )} + } cursor={{ fill: 'hsl(var(--muted) / 0.3)' }} /> + {showLegend && } + {bars.map((bar) => )} + + + ) +} + +export const BarChart = BarChartComponent +export type { BarChartProps } diff --git a/frontend/functions/ui/button.md b/frontend/functions/ui/button.md new file mode 100644 index 00000000..a4ffada6 --- /dev/null +++ b/frontend/functions/ui/button.md @@ -0,0 +1,53 @@ +--- +name: button +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Button(props: ButtonProps & VariantProps): JSX.Element" +description: "Botón accesible con 6 variantes (default, outline, secondary, ghost, destructive, link) y 8 tamaños. 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 + + + +``` + +## Notas + +Componente base del sistema. Usa Base-UI Button primitive para accesibilidad completa (keyboard, ARIA). CVA para gestión type-safe de variantes. diff --git a/frontend/functions/ui/button.tsx b/frontend/functions/ui/button.tsx new file mode 100644 index 00000000..74869947 --- /dev/null +++ b/frontend/functions/ui/button.tsx @@ -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) { + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/frontend/functions/ui/card.md b/frontend/functions/ui/card.md new file mode 100644 index 00000000..7de9a260 --- /dev/null +++ b/frontend/functions/ui/card.md @@ -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 + + + Título + Descripción + + Contenido + Footer + +``` + +## Notas + +Sistema de slots via data-slot attributes. Card detecta automáticamente la presencia de CardFooter y ajusta el padding. Exporta 7 subcomponentes composables. diff --git a/frontend/functions/ui/card.tsx b/frontend/functions/ui/card.tsx new file mode 100644 index 00000000..7f5a24df --- /dev/null +++ b/frontend/functions/ui/card.tsx @@ -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 ( +
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 ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent } diff --git a/frontend/functions/ui/chart_container.md b/frontend/functions/ui/chart_container.md new file mode 100644 index 00000000..7f163c01 --- /dev/null +++ b/frontend/functions/ui/chart_container.md @@ -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 + + ... + +``` + +## Notas + +Exporta: ChartContainer, ChartTooltipContent, ChartTooltip, ChartLegend, chartColors, defaultColors, getSeriesColor, Series. diff --git a/frontend/functions/ui/chart_container.tsx b/frontend/functions/ui/chart_container.tsx new file mode 100644 index 00000000..040a83ca --- /dev/null +++ b/frontend/functions/ui/chart_container.tsx @@ -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 ( +
+ + {children as React.ReactElement} + +
+ ) +} + +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 ( +
+

{labelFormatter(label || '')}

+
+ {payload.map((entry, index) => ( +
+
+ {entry.name}: + {valueFormatter(entry.value)} +
+ ))} +
+
+ ) +} + +export function ChartTooltip(props: React.ComponentProps) { + return } cursor={{ fill: 'hsl(var(--muted) / 0.3)' }} {...props} /> +} + +export function ChartLegend(props: React.ComponentProps) { + return +} diff --git a/frontend/functions/ui/crud_page.md b/frontend/functions/ui/crud_page.md new file mode 100644 index 00000000..9f139b00 --- /dev/null +++ b/frontend/functions/ui/crud_page.md @@ -0,0 +1,51 @@ +--- +name: crud_page +kind: function +lang: typescript +domain: ui +version: "1.0.0" +purity: pure +signature: "crudPage(props: CrudPageProps): ReactElement" +description: "Genera una página CRUD completa con header, tabla con columnas configurables, botones de acción (add/edit/delete) y schema de formulario." +tags: [crud, page, table, form, factory, composition, ui] +uses_functions: [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) => {v} }, + ], + onAdd: handleAdd, + onEdit: handleEdit, + onDelete: handleDelete, +}) +``` + +## Notas + +El schema de campos se almacena como data attribute para que un agente pueda leerlo y generar el formulario de diálogo correspondiente. La tabla incluye sorting visual implícito por columnas. diff --git a/frontend/functions/ui/crud_page.tsx b/frontend/functions/ui/crud_page.tsx new file mode 100644 index 00000000..effa6a24 --- /dev/null +++ b/frontend/functions/ui/crud_page.tsx @@ -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> { + title: string + subtitle?: string + data: T[] + fields: CrudField[] + columns: Array<{ + key: keyof T + label: string + render?: (value: unknown, row: T) => React.ReactNode + }> + onAdd?: (item: Partial) => void + onEdit?: (item: T) => void + onDelete?: (item: T) => void + actions?: React.ReactNode + className?: string +} + +export function crudPage>({ + title, + subtitle, + data, + fields, + columns, + onAdd, + onEdit, + onDelete, + actions, + className, +}: CrudPageProps): React.ReactElement { + return ( +
+ {/* Header */} +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ {actions} + {onAdd && ( + + )} +
+
+ + {/* Table */} +
+ + + + {columns.map((col) => ( + + ))} + {(onEdit || onDelete) && ( + + )} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((row, i) => ( + + {columns.map((col) => ( + + ))} + {(onEdit || onDelete) && ( + + )} + + )) + )} + +
+ {col.label} + Actions
+ No items yet. +
+ {col.render ? col.render(row[col.key], row) : String(row[col.key] ?? '')} + +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+
+
+ + {/* Form fields definition (for agent use — renders a form preview) */} +
+
+ ) +} + +export type { CrudPageProps, CrudField } diff --git a/frontend/functions/ui/dashboard_layout.md b/frontend/functions/ui/dashboard_layout.md new file mode 100644 index 00000000..e4b87676 --- /dev/null +++ b/frontend/functions/ui/dashboard_layout.md @@ -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: }, + { id: 'users', title: 'Users', content: }, + { id: 'chart', title: 'Trends', span: 2, content: }, + { id: 'table', span: 4, content: }, + ] +}) +``` + +## Notas + +Factory pura — dado el mismo input siempre genera el mismo JSX. Un agente puede construir dashboards completos pasando widgets como configuración declarativa. diff --git a/frontend/functions/ui/dashboard_layout.tsx b/frontend/functions/ui/dashboard_layout.tsx new file mode 100644 index 00000000..a3d3b89e --- /dev/null +++ b/frontend/functions/ui/dashboard_layout.tsx @@ -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 = { + 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 = { + 1: 'row-span-1', + 2: 'row-span-2', +} + +export function dashboardLayout({ + widgets, + columns = 4, + gap = 'md', + className, +}: DashboardLayoutProps): React.ReactElement { + const gridCols: Record = { + 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 ( +
+ {widgets.map((widget) => ( +
+ {widget.title && ( +

{widget.title}

+ )} + {widget.content} +
+ ))} +
+ ) +} + +export type { DashboardWidget, DashboardLayoutProps } diff --git a/frontend/functions/ui/detail_page.md b/frontend/functions/ui/detail_page.md new file mode 100644 index 00000000..3eb5c5bb --- /dev/null +++ b/frontend/functions/ui/detail_page.md @@ -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: Active, + onBack: () => router.back(), + fields: [ + { label: 'Role', value: 'Administrator' }, + { label: 'Created', value: 'Mar 15, 2026' }, + { label: 'Bio', value: 'Full stack developer...', span: 2 }, + ], + tabs: [ + { label: 'Projects', value: 'projects', count: 12, content: }, + { label: 'Activity', value: 'activity', count: 48, content: }, + ], + activeTab: 'projects', + timeline: [ + { id: '1', title: 'Deployed v2.1', timestamp: '2 hours ago', variant: 'success' }, + { id: '2', title: 'Updated settings', timestamp: 'Yesterday' }, + { id: '3', title: 'Created project', timestamp: 'Mar 10, 2026' }, + ], +}) +``` + +## Notas + +Factory completa para páginas de detalle. Combina header con back/avatar/badge, grid de metadata, tabs con badges de conteo, y timeline de actividad con variantes de color semántico. diff --git a/frontend/functions/ui/detail_page.tsx b/frontend/functions/ui/detail_page.tsx new file mode 100644 index 00000000..86bae0d4 --- /dev/null +++ b/frontend/functions/ui/detail_page.tsx @@ -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 ( +
+ {/* Header */} +
+
+ {onBack && ( + + )} + {avatar &&
{avatar}
} +
+
+

{title}

+ {badge} +
+ {subtitle &&

{subtitle}

} +
+
+ {actions &&
{actions}
} +
+ + {/* Fields grid */} +
+ {fields.map((field, i) => ( +
+

{field.label}

+
{field.value}
+
+ ))} +
+ + {/* Tabs */} + {tabs && tabs.length > 0 && ( +
+ + {tabs.find(t => t.value === activeTab)?.content} +
+ )} + + {/* Timeline */} + {timeline && timeline.length > 0 && ( +
+

Activity

+
+ {timeline.map((event, i) => ( +
+
+
+ {i < timeline.length - 1 &&
} +
+
+

{event.title}

+ {event.description &&

{event.description}

} +

{event.timestamp}

+
+
+ ))} +
+
+ )} +
+ ) +} + +export type { DetailPageProps, DetailField, DetailTab, TimelineEvent } diff --git a/frontend/functions/ui/dialog.md b/frontend/functions/ui/dialog.md new file mode 100644 index 00000000..f4cf5991 --- /dev/null +++ b/frontend/functions/ui/dialog.md @@ -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 + + + + + Título + Descripción + +

Contenido

+ + + +
+
+``` + +## Notas + +10 subcomponentes exportados. Base-UI Dialog primitive para accesibilidad completa (focus trap, escape, click outside). diff --git a/frontend/functions/ui/dialog.tsx b/frontend/functions/ui/dialog.tsx new file mode 100644 index 00000000..77c7d8fa --- /dev/null +++ b/frontend/functions/ui/dialog.tsx @@ -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 +} + +function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { + return +} + +function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { + return +} + +function DialogClose({ ...props }: DialogPrimitive.Close.Props) { + return +} + +function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) { + return ( + + ) +} + +function DialogContent({ className, children, showCloseButton = true, ...props }: DialogPrimitive.Popup.Props & { showCloseButton?: boolean }) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return
+} + +function DialogFooter({ className, children, ...props }: React.ComponentProps<"div">) { + return ( +
+ {children} +
+ ) +} + +function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { + return +} + +function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) { + return +} + +export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } diff --git a/frontend/functions/ui/form_field.md b/frontend/functions/ui/form_field.md new file mode 100644 index 00000000..81035466 --- /dev/null +++ b/frontend/functions/ui/form_field.md @@ -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 + + + +``` diff --git a/frontend/functions/ui/form_field.tsx b/frontend/functions/ui/form_field.tsx new file mode 100644 index 00000000..d238aa43 --- /dev/null +++ b/frontend/functions/ui/form_field.tsx @@ -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>, { + id: inputId, + "aria-invalid": error ? true : undefined, + "aria-describedby": describedBy, + }) + } + return child + }) + + return ( +
+ {label && } + {childWithProps} + {helperText && !error &&

{helperText}

} + {error &&

{error}

} +
+ ) +} + +export { FormField } +export type { FormFieldProps } diff --git a/frontend/functions/ui/input.md b/frontend/functions/ui/input.md new file mode 100644 index 00000000..10dbe0f9 --- /dev/null +++ b/frontend/functions/ui/input.md @@ -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 + + + + + +``` + +## Notas + +Exporta Input, InputGroup e InputIcon. InputGroup detecta automáticamente la presencia de iconos y ajusta padding del Input. diff --git a/frontend/functions/ui/input.tsx b/frontend/functions/ui/input.tsx new file mode 100644 index 00000000..e5168797 --- /dev/null +++ b/frontend/functions/ui/input.tsx @@ -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 ( + + ) +} + +interface InputGroupProps { + children: React.ReactNode + className?: string +} + +function InputGroup({ children, className }: InputGroupProps) { + return ( +
+ {children} +
+ ) +} + +interface InputIconProps { + children: React.ReactNode + position: "start" | "end" + className?: string +} + +function InputIcon({ children, position, className }: InputIconProps) { + return ( + + {children} + + ) +} + +export { Input, InputGroup, InputIcon } diff --git a/frontend/functions/ui/kpi_card.md b/frontend/functions/ui/kpi_card.md new file mode 100644 index 00000000..b22b2965 --- /dev/null +++ b/frontend/functions/ui/kpi_card.md @@ -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 + +} /> +``` diff --git a/frontend/functions/ui/kpi_card.tsx b/frontend/functions/ui/kpi_card.tsx new file mode 100644 index 00000000..d45a50b8 --- /dev/null +++ b/frontend/functions/ui/kpi_card.tsx @@ -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 { + label: string + value: string | number + delta?: Delta + icon?: React.ReactNode + subtitle?: string + size?: KPICardSize +} + +const sizeStyles: Record = { + 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( + ({ 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 ( +
+
+
+

{label}

+ {subtitle &&

{subtitle}

} +
+ {icon &&
{icon}
} +
+
+
+

{value}

+ {delta && ( +
+ {delta.value > 0 ? '+' : ''}{delta.value}% +
+ )} +
+
+
+ ) + } +) +KPICard.displayName = 'KPICard' + +export { KPICard, type KPICardProps, type Delta, type KPICardSize } diff --git a/frontend/functions/ui/label.md b/frontend/functions/ui/label.md new file mode 100644 index 00000000..73b31c7c --- /dev/null +++ b/frontend/functions/ui/label.md @@ -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 + +``` diff --git a/frontend/functions/ui/label.tsx b/frontend/functions/ui/label.tsx new file mode 100644 index 00000000..56bb58a6 --- /dev/null +++ b/frontend/functions/ui/label.tsx @@ -0,0 +1,17 @@ +import * as React from "react" +import { cn } from "../core/cn" + +function Label({ className, ...props }: React.ComponentProps<"label">) { + return ( +