diff --git a/frontend/functions/ui/accordion.md b/frontend/functions/ui/accordion.md
new file mode 100644
index 00000000..f66db4cb
--- /dev/null
+++ b/frontend/functions/ui/accordion.md
@@ -0,0 +1,53 @@
+---
+name: accordion
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Accordion(props: AccordionProps): JSX.Element"
+description: "Secciones colapsables con animaciones. Base-UI Collapsible primitive. Composable: AccordionItem + AccordionTrigger + AccordionContent."
+tags: [accordion, collapsible, component, ui, interactive, base-ui]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["@base-ui/react/collapsible", "lucide-react"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/accordion.tsx"
+props:
+ - name: className
+ type: "string"
+ required: false
+ description: "Clases CSS adicionales para el contenedor"
+emits: []
+has_state: false
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+
+
+ Seccion 1
+
+ Contenido de la primera seccion.
+
+
+
+ Seccion 2
+
+ Contenido de la segunda seccion.
+
+
+
+```
+
+## Notas
+
+Cada AccordionItem es un Collapsible independiente — permite multiples items abiertos simultaneamente. Para exclusividad (solo uno abierto), manejar el estado externamente. El chevron rota 180 grados con [data-open]. Exports: Accordion, AccordionItem, AccordionTrigger, AccordionContent.
diff --git a/frontend/functions/ui/accordion.tsx b/frontend/functions/ui/accordion.tsx
new file mode 100644
index 00000000..c7acabcf
--- /dev/null
+++ b/frontend/functions/ui/accordion.tsx
@@ -0,0 +1,81 @@
+import * as React from "react"
+import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
+import { ChevronDownIcon } from "lucide-react"
+import { cn } from "../core/cn"
+
+interface AccordionItem {
+ value: string
+ trigger: React.ReactNode
+ content: React.ReactNode
+ disabled?: boolean
+}
+
+interface AccordionProps {
+ items?: AccordionItem[]
+ type?: "single" | "multiple"
+ defaultValue?: string | string[]
+ className?: string
+ itemClassName?: string
+ children?: React.ReactNode
+}
+
+function Accordion({ className, children, ...props }: React.ComponentProps<"div"> & AccordionProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+interface AccordionItemProps extends CollapsiblePrimitive.Root.Props {
+ className?: string
+}
+
+function AccordionItem({ className, ...props }: AccordionItemProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({ className, children, ...props }: CollapsiblePrimitive.Trigger.Props) {
+ return (
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+ )
+}
+
+function AccordionContent({ className, children, ...props }: CollapsiblePrimitive.Panel.Props) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
+export type { AccordionItem as AccordionItemData, AccordionProps }
diff --git a/frontend/functions/ui/avatar.md b/frontend/functions/ui/avatar.md
new file mode 100644
index 00000000..c153f024
--- /dev/null
+++ b/frontend/functions/ui/avatar.md
@@ -0,0 +1,70 @@
+---
+name: avatar
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Avatar(props: AvatarProps): JSX.Element"
+description: "Imagen de usuario circular con fallback a iniciales generadas automaticamente. 5 tamaños via CVA."
+tags: [avatar, user, image, component, ui, cva]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["class-variance-authority"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/avatar.tsx"
+props:
+ - name: src
+ type: "string"
+ required: false
+ description: "URL de la imagen"
+ - name: alt
+ type: "string"
+ required: false
+ description: "Texto alternativo de la imagen"
+ - name: fallback
+ type: "string"
+ required: false
+ description: "Nombre completo del que extraer iniciales (ej: 'Juan Perez' -> 'JP')"
+ - name: initials
+ type: "string"
+ required: false
+ description: "Iniciales explicitas para el fallback (sobrescribe fallback)"
+ - name: size
+ type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'"
+ required: false
+ description: "Tamanio del avatar (default: md)"
+ - name: className
+ type: "string"
+ required: false
+ description: "Clases CSS adicionales"
+emits: []
+has_state: true
+framework: react
+variant: [xs, sm, md, lg, xl]
+---
+
+## Ejemplo
+
+```tsx
+// Con imagen
+
+
+// Con fallback a iniciales
+
+
+// Iniciales explicitas
+
+
+// Maneja error de imagen automaticamente
+
+```
+
+## Notas
+
+Usa estado interno para manejar errores de carga de imagen (onError). La funcion getInitials extrae 2 iniciales del nombre completo (primera y ultima palabra). Si solo hay una palabra, toma los 2 primeros caracteres. Usa forwardRef para compatibilidad con wrappers.
diff --git a/frontend/functions/ui/avatar.tsx b/frontend/functions/ui/avatar.tsx
new file mode 100644
index 00000000..012bdae3
--- /dev/null
+++ b/frontend/functions/ui/avatar.tsx
@@ -0,0 +1,69 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { cn } from "../core/cn"
+
+const avatarVariants = cva(
+ "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground select-none",
+ {
+ variants: {
+ size: {
+ xs: "size-6 text-xs",
+ sm: "size-8 text-sm",
+ md: "size-10 text-base",
+ lg: "size-12 text-lg",
+ xl: "size-16 text-xl",
+ },
+ },
+ defaultVariants: { size: "md" },
+ }
+)
+
+interface AvatarProps
+ extends React.ComponentPropsWithoutRef<"span">,
+ VariantProps {
+ src?: string
+ alt?: string
+ fallback?: string
+ initials?: string
+}
+
+function getInitials(name?: string): string {
+ if (!name) return "?"
+ const parts = name.trim().split(/\s+/)
+ if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
+}
+
+const Avatar = React.forwardRef(
+ ({ className, size, src, alt, fallback, initials, ...props }, ref) => {
+ const [imgError, setImgError] = React.useState(false)
+ const showImage = src && !imgError
+ const displayInitials = initials ?? getInitials(fallback ?? alt)
+
+ return (
+
+ {showImage ? (
+
setImgError(true)}
+ />
+ ) : (
+
+ {displayInitials}
+
+ )}
+
+ )
+ }
+)
+Avatar.displayName = "Avatar"
+
+export { Avatar, avatarVariants }
+export type { AvatarProps }
diff --git a/frontend/functions/ui/breadcrumb.md b/frontend/functions/ui/breadcrumb.md
new file mode 100644
index 00000000..c2d788c8
--- /dev/null
+++ b/frontend/functions/ui/breadcrumb.md
@@ -0,0 +1,71 @@
+---
+name: breadcrumb
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Breadcrumb(props: BreadcrumbProps): JSX.Element"
+description: "Navegacion jerarquica con separadores, elipsis para paths largos y soporte para router links via asChild."
+tags: [breadcrumb, navigation, component, ui]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["lucide-react"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/breadcrumb.tsx"
+props:
+ - name: className
+ type: "string"
+ required: false
+ description: "Clases CSS adicionales"
+emits: []
+has_state: false
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+
+
+
+ Inicio
+
+
+
+ Documentacion
+
+
+
+ Componentes
+
+
+
+
+// Con elipsis para paths largos
+
+
+
+ Inicio
+
+
+
+
+
+
+
+ Pagina actual
+
+
+
+```
+
+## Notas
+
+Exports: Breadcrumb (nav), BreadcrumbList (ol), BreadcrumbItem (li), BreadcrumbLink (a con asChild), BreadcrumbPage (span aria-current=page), BreadcrumbSeparator (ChevronRight por defecto, customizable), BreadcrumbEllipsis (MoreHorizontal). BreadcrumbLink acepta asChild para usar con Link de React Router o Next.js.
diff --git a/frontend/functions/ui/breadcrumb.tsx b/frontend/functions/ui/breadcrumb.tsx
new file mode 100644
index 00000000..8cb47f23
--- /dev/null
+++ b/frontend/functions/ui/breadcrumb.tsx
@@ -0,0 +1,97 @@
+import * as React from "react"
+import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
+import { cn } from "../core/cn"
+
+function Breadcrumb({ ...props }: React.ComponentPropsWithoutRef<"nav">) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentPropsWithoutRef<"ol">) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentPropsWithoutRef<"li">) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ className,
+ href,
+ asChild,
+ children,
+ ...props
+}: React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }) {
+ if (asChild) {
+ return (
+ )}>
+ {children}
+
+ )
+ }
+ return (
+
+ {children}
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
+ return (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
+ return (
+
+
+ More
+
+ )
+}
+
+export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator }
diff --git a/frontend/functions/ui/checkbox.md b/frontend/functions/ui/checkbox.md
new file mode 100644
index 00000000..b96ac941
--- /dev/null
+++ b/frontend/functions/ui/checkbox.md
@@ -0,0 +1,72 @@
+---
+name: checkbox
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Checkbox(props: CheckboxProps): JSX.Element"
+description: "Input booleano accesible con label opcional y variante indeterminate. Base-UI Checkbox primitive."
+tags: [checkbox, component, ui, interactive, form, base-ui]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["@base-ui/react/checkbox", "class-variance-authority"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/checkbox.tsx"
+props:
+ - name: label
+ type: "string"
+ required: false
+ description: "Texto de etiqueta visible junto al checkbox"
+ - name: indeterminate
+ type: "boolean"
+ required: false
+ description: "Estado indeterminate (guion) en vez de checked/unchecked"
+ - name: checked
+ type: "boolean"
+ required: false
+ description: "Estado controlado del checkbox"
+ - name: defaultChecked
+ type: "boolean"
+ required: false
+ description: "Estado inicial no controlado"
+ - name: disabled
+ type: "boolean"
+ required: false
+ description: "Deshabilita el checkbox"
+ - name: onCheckedChange
+ type: "(checked: boolean) => void"
+ required: false
+ description: "Callback cuando cambia el estado"
+emits: [onCheckedChange]
+has_state: false
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+// Basico
+
+
+// Controlado
+
+
+// Sin label
+
+```
+
+## Notas
+
+Usa Base-UI Checkbox primitive para accesibilidad completa (keyboard, ARIA). El estado indeterminate se muestra con un guion horizontal. El id se genera automaticamente con useId si no se provee.
diff --git a/frontend/functions/ui/checkbox.tsx b/frontend/functions/ui/checkbox.tsx
new file mode 100644
index 00000000..117afce8
--- /dev/null
+++ b/frontend/functions/ui/checkbox.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
+import { CheckboxIndicator } from "@base-ui/react/checkbox"
+import { cn } from "../core/cn"
+
+interface CheckboxProps extends CheckboxPrimitive.Root.Props {
+ label?: string
+ indeterminate?: boolean
+ className?: string
+ labelClassName?: string
+}
+
+function Checkbox({ className, label, id, indeterminate, ...props }: CheckboxProps) {
+ const internalId = React.useId()
+ const checkboxId = id ?? internalId
+
+ return (
+
+
+
+ {indeterminate ? (
+
+ ) : (
+
+ )}
+
+
+ {label && (
+
+ )}
+
+ )
+}
+
+export { Checkbox }
+export type { CheckboxProps }
diff --git a/frontend/functions/ui/command.md b/frontend/functions/ui/command.md
new file mode 100644
index 00000000..22c466a2
--- /dev/null
+++ b/frontend/functions/ui/command.md
@@ -0,0 +1,81 @@
+---
+name: command
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Command(props: CommandProps): JSX.Element"
+description: "Combobox de busqueda y seleccion estilo cmdk. Filtra items por query, soporta grupos, iconos y shortcuts. Incluye CommandSearch para uso de una linea."
+tags: [command, search, combobox, component, ui, interactive]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["lucide-react"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/command.tsx"
+props:
+ - name: items
+ type: "CommandItem[]"
+ required: true
+ description: "Array de items con value, label, description, icon, disabled, group"
+ - name: value
+ type: "string"
+ required: false
+ description: "Valor seleccionado (controlado)"
+ - name: onValueChange
+ type: "(value: string) => void"
+ required: false
+ description: "Callback al seleccionar un item"
+ - name: placeholder
+ type: "string"
+ required: false
+ description: "Placeholder del input de busqueda (default: Search...)"
+ - name: emptyMessage
+ type: "string"
+ required: false
+ description: "Mensaje cuando no hay resultados (default: No results found.)"
+emits: [onValueChange]
+has_state: true
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+// Uso simple con CommandSearch
+const items = [
+ { value: "react", label: "React", group: "Frameworks" },
+ { value: "vue", label: "Vue", group: "Frameworks" },
+ { value: "typescript", label: "TypeScript", group: "Lenguajes" },
+]
+
+ console.log(val)}
+/>
+
+// Composable para mayor control
+
+ setQuery(e.target.value)} />
+
+ Sin resultados.
+
+ setSelected("1")}>
+ Opcion 1
+ ⌘K
+
+
+
+
+```
+
+## Notas
+
+Implementacion propia (sin dependencia de cmdk) usando primitivos HTML nativos. CommandSearch es el wrapper de alto nivel con filtrado reactivo integrado. El filtrado es case-insensitive sobre label, description y value. Los grupos se renderizan en orden de aparicion en items.
diff --git a/frontend/functions/ui/command.tsx b/frontend/functions/ui/command.tsx
new file mode 100644
index 00000000..aa377e0c
--- /dev/null
+++ b/frontend/functions/ui/command.tsx
@@ -0,0 +1,204 @@
+import * as React from "react"
+import { SearchIcon, XIcon } from "lucide-react"
+import { cn } from "../core/cn"
+
+interface CommandItem {
+ value: string
+ label: string
+ description?: string
+ icon?: React.ReactNode
+ disabled?: boolean
+ group?: string
+}
+
+interface CommandProps {
+ items: CommandItem[]
+ value?: string
+ onValueChange?: (value: string) => void
+ placeholder?: string
+ emptyMessage?: string
+ className?: string
+ inputClassName?: string
+ listClassName?: string
+}
+
+function Command({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
+ return (
+
+ )
+}
+
+function CommandInput({ className, ...props }: React.ComponentPropsWithoutRef<"input">) {
+ return (
+
+
+
+
+ )
+}
+
+function CommandList({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
+ return (
+
+ )
+}
+
+function CommandEmpty({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
+ return (
+
+ )
+}
+
+function CommandGroup({ className, heading, ...props }: React.ComponentPropsWithoutRef<"div"> & { heading?: string }) {
+ return (
+
+ {heading && (
+
{heading}
+ )}
+
+
+ )
+}
+
+function CommandSeparator({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
+ return (
+
+ )
+}
+
+interface CommandItemProps extends React.ComponentPropsWithoutRef<"div"> {
+ selected?: boolean
+ disabled?: boolean
+ onSelect?: () => void
+}
+
+function CommandItem({ className, selected, disabled, onSelect, ...props }: CommandItemProps) {
+ return (
+
+ )
+}
+
+function CommandShortcut({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
+ return (
+
+ )
+}
+
+function CommandSearch({
+ items,
+ value,
+ onValueChange,
+ placeholder = "Search...",
+ emptyMessage = "No results found.",
+ className,
+}: CommandProps) {
+ const [query, setQuery] = React.useState("")
+ const [selectedValue, setSelectedValue] = React.useState(value ?? "")
+
+ const filtered = React.useMemo(() => {
+ if (!query) return items
+ const q = query.toLowerCase()
+ return items.filter(
+ (item) =>
+ item.label.toLowerCase().includes(q) ||
+ item.description?.toLowerCase().includes(q) ||
+ item.value.toLowerCase().includes(q)
+ )
+ }, [items, query])
+
+ const groups = React.useMemo(() => {
+ const map = new Map()
+ for (const item of filtered) {
+ const key = item.group ?? ""
+ if (!map.has(key)) map.set(key, [])
+ map.get(key)!.push(item)
+ }
+ return map
+ }, [filtered])
+
+ const handleSelect = (val: string) => {
+ setSelectedValue(val)
+ onValueChange?.(val)
+ }
+
+ return (
+
+ setQuery(e.target.value)}
+ placeholder={placeholder}
+ />
+
+ {filtered.length === 0 ? (
+ {emptyMessage}
+ ) : (
+ Array.from(groups.entries()).map(([group, groupItems]) => (
+
+ {groupItems.map((item) => (
+ handleSelect(item.value)}
+ >
+ {item.icon && {item.icon}}
+ {item.label}
+ {item.description && (
+ {item.description}
+ )}
+
+ ))}
+
+ ))
+ )}
+
+
+ )
+}
+
+export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut }
+export type { CommandItem, CommandProps }
diff --git a/frontend/functions/ui/dropdown_menu.md b/frontend/functions/ui/dropdown_menu.md
new file mode 100644
index 00000000..30563419
--- /dev/null
+++ b/frontend/functions/ui/dropdown_menu.md
@@ -0,0 +1,73 @@
+---
+name: dropdown_menu
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "DropdownMenu(props: DropdownMenuProps): JSX.Element"
+description: "Menu de acciones y contexto accesible con items, checkboxes, radios, separadores y submenus. Base-UI Menu primitive."
+tags: [dropdown, menu, component, ui, interactive, overlay, base-ui]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["@base-ui/react/menu", "lucide-react"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/dropdown_menu.tsx"
+props:
+ - name: open
+ type: "boolean"
+ required: false
+ description: "Estado controlado de apertura"
+ - name: defaultOpen
+ type: "boolean"
+ required: false
+ description: "Estado inicial de apertura"
+ - name: onOpenChange
+ type: "(open: boolean) => void"
+ required: false
+ description: "Callback cuando cambia el estado"
+ - name: modal
+ type: "boolean"
+ required: false
+ description: "Comportamiento modal (default: true)"
+emits: [onOpenChange]
+has_state: false
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+
+
+
+
+
+ Mi cuenta
+
+ console.log("Perfil")}>
+ Perfil
+
+
+ Marcadores
+
+
+
+ Mas opciones
+
+ Opcion A
+
+
+
+
+```
+
+## Notas
+
+Exports: DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuPortal.
diff --git a/frontend/functions/ui/dropdown_menu.tsx b/frontend/functions/ui/dropdown_menu.tsx
new file mode 100644
index 00000000..3fb24bea
--- /dev/null
+++ b/frontend/functions/ui/dropdown_menu.tsx
@@ -0,0 +1,201 @@
+import * as React from "react"
+import { Menu as MenuPrimitive } from "@base-ui/react/menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+import { cn } from "../core/cn"
+
+function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
+ return
+}
+
+function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
+ return
+}
+
+function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
+ return
+}
+
+function DropdownMenuContent({ className, sideOffset = 4, ...props }: MenuPrimitive.Positioner.Props) {
+ return (
+
+
+
+ {props.children}
+
+
+
+ )
+}
+
+function DropdownMenuItem({ className, inset, ...props }: MenuPrimitive.Item.Props & { inset?: boolean }) {
+ return (
+
+ )
+}
+
+function DropdownMenuCheckboxItem({ className, children, checked, ...props }: MenuPrimitive.CheckboxItem.Props) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
+ return
+}
+
+function DropdownMenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
+ return
+}
+
+function DropdownMenuLabel({ className, inset, ...props }: MenuPrimitive.GroupLabel.Props & { inset?: boolean }) {
+ return (
+
+ )
+}
+
+function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function DropdownMenuSub({ ...props }: MenuPrimitive.Root.Props) {
+ return
+}
+
+function DropdownMenuSubTrigger({ className, inset, children, ...props }: MenuPrimitive.SubmenuTrigger.Props & { inset?: boolean }) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function DropdownMenuSubContent({ className, ...props }: MenuPrimitive.Positioner.Props) {
+ return (
+
+
+
+ {props.children}
+
+
+
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+}
diff --git a/frontend/functions/ui/index.ts b/frontend/functions/ui/index.ts
new file mode 100644
index 00000000..e31dcd5c
--- /dev/null
+++ b/frontend/functions/ui/index.ts
@@ -0,0 +1,121 @@
+// Barrel export — @fn_library
+// Primitives
+export { Alert, AlertTitle, AlertDescription } from './alert'
+export { Badge, badgeVariants } from './badge'
+export { Button, buttonVariants } from './button'
+export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent } from './card'
+export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './dialog'
+export { Input, InputGroup, InputIcon } from './input'
+export { Label } from './label'
+export { KPICard } from './kpi_card'
+export { Select, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectPortal, SelectSeparator, SelectTrigger, SelectValue } from './select'
+export { SimpleSelect } from './simple_select'
+export type { SimpleSelectOption, SimpleSelectGroup, SimpleSelectOptions } from './simple_select'
+export { Skeleton, SkeletonAvatar, SkeletonButton, SkeletonCard, SkeletonTable, SkeletonText } from './skeleton'
+export { Sparkline } from './sparkline'
+export type { SparklineProps, SparklineVariant } from './sparkline'
+export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs'
+export { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger } from './tooltip'
+export { FormField } from './form_field'
+export type { FormFieldProps } from './form_field'
+export { PageHeader } from './page_header'
+export { ProgressBar } from './progress_bar'
+
+// Charts
+export { AreaChart } from './area_chart'
+export type { AreaChartProps, GradientConfig } from './area_chart'
+export { BarChart } from './bar_chart'
+export type { BarChartProps } from './bar_chart'
+export { LineChart } from './line_chart'
+export type { LineChartProps, CurveType } from './line_chart'
+export { PieChart } from './pie_chart'
+export type { PieChartProps } from './pie_chart'
+export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, getSeriesColor } from './chart_container'
+export type { Series } from './chart_container'
+
+// Data
+export { DataTable } from './data_table'
+export type { DataTableProps, ColumnDef } from './data_table'
+
+// Theme
+export { ThemeProvider, useTheme, ThemeContext } from './theme_provider'
+export type { ThemeProviderProps } from './theme_provider'
+export { applyTheme } from './apply_theme'
+export type { Theme, ThemeColors } from './apply_theme'
+
+// Page templates
+export { analyticsPage } from './analytics_page'
+export type { AnalyticsPageProps, MetricConfig, ChartConfig } from './analytics_page'
+export { crudPage } from './crud_page'
+export type { CrudPageProps, CrudField } from './crud_page'
+export { dashboardLayout } from './dashboard_layout'
+export type { DashboardWidget, DashboardLayoutProps } from './dashboard_layout'
+export { detailPage } from './detail_page'
+export type { DetailPageProps, DetailField, DetailTab, TimelineEvent } from './detail_page'
+export { settingsPage } from './settings_page'
+export type { SettingsPageProps, SettingSection, SettingField } from './settings_page'
+
+// Hooks — Wails
+export { useWailsQuery } from './use_wails_query'
+export type { UseWailsQueryOptions, UseWailsQueryResult } from './use_wails_query'
+export { useWailsMutation } from './use_wails_mutation'
+export type { UseWailsMutationOptions, UseWailsMutationResult } from './use_wails_mutation'
+export { useWailsStream, useWailsLogs } from './use_wails_stream'
+export type { UseWailsStreamOptions, UseWailsStreamResult } from './use_wails_stream'
+export { useWailsEvent, useWailsEmit } from './use_wails_event'
+export type { UseWailsEventOptions, UseWailsEventResult } from './use_wails_event'
+
+// Accordion
+export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './accordion'
+export type { AccordionProps } from './accordion'
+
+// Avatar
+export { Avatar, avatarVariants } from './avatar'
+export type { AvatarProps } from './avatar'
+
+// Breadcrumb
+export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from './breadcrumb'
+
+// Checkbox
+export { Checkbox } from './checkbox'
+export type { CheckboxProps } from './checkbox'
+
+// Command
+export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut } from './command'
+export type { CommandProps } from './command'
+
+// Dropdown Menu
+export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from './dropdown_menu'
+
+// Pagination
+export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from './pagination'
+export type { PaginationLinkProps } from './pagination'
+
+// Popover
+export { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverPortal, PopoverTitle, PopoverTrigger } from './popover'
+
+// Radio Group
+export { RadioGroup, RadioGroupItem } from './radio_group'
+export type { RadioGroupItemProps } from './radio_group'
+
+// Sheet
+export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, sheetVariants } from './sheet'
+export type { SheetContentProps } from './sheet'
+
+// Switch
+export { SwitchToggle } from './switch_toggle'
+export type { SwitchToggleProps } from './switch_toggle'
+
+// Textarea
+export { Textarea } from './textarea'
+export type { TextareaProps } from './textarea'
+
+// Toast
+export { Toast, ToastProvider, ToastViewport, toastVariants, useToast } from './toast'
+export type { ToastEntry, ToastProps, ToastViewportProps } from './toast'
+
+// Hooks — Canvas
+export { useAnimatedCanvas } from './use_animated_canvas'
+
+// Wails Provider
+export { WailsProvider } from './wails_provider'
diff --git a/frontend/functions/ui/pagination.md b/frontend/functions/ui/pagination.md
new file mode 100644
index 00000000..4844e1f2
--- /dev/null
+++ b/frontend/functions/ui/pagination.md
@@ -0,0 +1,61 @@
+---
+name: pagination
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Pagination(props: PaginationProps): JSX.Element"
+description: "Controles de navegacion de paginas con Previous/Next, numeros de pagina, elipsis y estado activo."
+tags: [pagination, navigation, component, ui]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["lucide-react", "./button"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/pagination.tsx"
+props:
+ - name: className
+ type: "string"
+ required: false
+ description: "Clases CSS adicionales"
+emits: []
+has_state: false
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+
+
+
+
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+
+
+
+
+
+
+
+```
+
+## Notas
+
+Exports: Pagination (nav), PaginationContent (ul), PaginationItem (li), PaginationLink (a con isActive/disabled), PaginationPrevious, PaginationNext, PaginationEllipsis. PaginationLink reutiliza buttonVariants para consistencia visual. Componente presentacional — el manejo del estado de pagina queda en el consumidor.
diff --git a/frontend/functions/ui/pagination.tsx b/frontend/functions/ui/pagination.tsx
new file mode 100644
index 00000000..6771cf31
--- /dev/null
+++ b/frontend/functions/ui/pagination.tsx
@@ -0,0 +1,100 @@
+import * as React from "react"
+import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
+import { cn } from "../core/cn"
+import { buttonVariants } from "./button"
+
+function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
+ return (
+
+ )
+}
+
+function PaginationContent({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function PaginationItem({ ...props }: React.ComponentProps<"li">) {
+ return
+}
+
+type PaginationLinkProps = {
+ isActive?: boolean
+ disabled?: boolean
+ size?: "icon" | "default" | "sm" | "lg"
+} & React.ComponentProps<"a">
+
+function PaginationLink({ className, isActive, disabled, size = "icon", ...props }: PaginationLinkProps) {
+ return (
+
+ )
+}
+
+function PaginationPrevious({ className, ...props }: React.ComponentProps<"a">) {
+ return (
+
+
+ Previous
+
+ )
+}
+
+function PaginationNext({ className, ...props }: React.ComponentProps<"a">) {
+ return (
+
+ Next
+
+
+ )
+}
+
+function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) {
+ return (
+
+
+ More pages
+
+ )
+}
+
+export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious }
+export type { PaginationLinkProps }
diff --git a/frontend/functions/ui/popover.md b/frontend/functions/ui/popover.md
new file mode 100644
index 00000000..c909bcce
--- /dev/null
+++ b/frontend/functions/ui/popover.md
@@ -0,0 +1,65 @@
+---
+name: popover
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Popover(props: PopoverProps): JSX.Element"
+description: "Contenido flotante posicionado accesible con animaciones. Base-UI Popover primitive."
+tags: [popover, component, ui, interactive, overlay, base-ui]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["@base-ui/react/popover"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/popover.tsx"
+props:
+ - name: open
+ type: "boolean"
+ required: false
+ description: "Estado controlado de apertura"
+ - name: defaultOpen
+ type: "boolean"
+ required: false
+ description: "Estado inicial de apertura (no controlado)"
+ - name: onOpenChange
+ type: "(open: boolean) => void"
+ required: false
+ description: "Callback cuando cambia el estado de apertura"
+ - name: sideOffset
+ type: "number"
+ required: false
+ description: "Distancia en px entre trigger y popover (default: 4)"
+emits: [onOpenChange]
+has_state: false
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+
+
+
+
+
+
+ Configuracion
+ Ajusta tus preferencias.
+
+
+ {/* contenido */}
+
+
+
+```
+
+## Notas
+
+Compuesto de: Popover (root), PopoverTrigger, PopoverContent (positioner + popup), PopoverClose, PopoverHeader, PopoverTitle, PopoverDescription. El posicionamiento automatico lo maneja Base-UI. Animaciones con data-open/data-closed.
diff --git a/frontend/functions/ui/popover.tsx b/frontend/functions/ui/popover.tsx
new file mode 100644
index 00000000..d82f346e
--- /dev/null
+++ b/frontend/functions/ui/popover.tsx
@@ -0,0 +1,57 @@
+import * as React from "react"
+import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
+import { cn } from "../core/cn"
+
+function Popover({ ...props }: PopoverPrimitive.Root.Props) {
+ return
+}
+
+function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
+ return
+}
+
+function PopoverPortal({ ...props }: PopoverPrimitive.Portal.Props) {
+ return
+}
+
+function PopoverContent({ className, sideOffset = 4, ...props }: PopoverPrimitive.Positioner.Props) {
+ return (
+
+
+
+ {props.children}
+
+
+
+ )
+}
+
+function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
+ return
+}
+
+function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return
+}
+
+function PopoverTitle({ className, ...props }: React.ComponentProps<"h4">) {
+ return
+}
+
+function PopoverDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return
+}
+
+export { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverPortal, PopoverTitle, PopoverTrigger }
diff --git a/frontend/functions/ui/radio_group.md b/frontend/functions/ui/radio_group.md
new file mode 100644
index 00000000..b331c4ff
--- /dev/null
+++ b/frontend/functions/ui/radio_group.md
@@ -0,0 +1,60 @@
+---
+name: radio_group
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "RadioGroup(props: RadioGroupProps): JSX.Element"
+description: "Grupo de opciones exclusivas accesible. Base-UI RadioGroup + Radio primitives."
+tags: [radio, radio-group, component, ui, interactive, form, base-ui]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["@base-ui/react/radio-group", "@base-ui/react/radio"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/radio_group.tsx"
+props:
+ - name: value
+ type: "string"
+ required: false
+ description: "Valor seleccionado (controlado)"
+ - name: defaultValue
+ type: "string"
+ required: false
+ description: "Valor inicial (no controlado)"
+ - name: onValueChange
+ type: "(value: string) => void"
+ required: false
+ description: "Callback al cambiar seleccion"
+ - name: disabled
+ type: "boolean"
+ required: false
+ description: "Deshabilita todo el grupo"
+ - name: orientation
+ type: "'horizontal' | 'vertical'"
+ required: false
+ description: "Orientacion del grupo"
+emits: [onValueChange]
+has_state: false
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+
+
+
+
+
+```
+
+## Notas
+
+RadioGroup es el contenedor (Base-UI RadioGroup). RadioGroupItem es cada opcion individual (Base-UI Radio). El id de cada item se genera con useId si no se provee.
diff --git a/frontend/functions/ui/radio_group.tsx b/frontend/functions/ui/radio_group.tsx
new file mode 100644
index 00000000..08cd22fa
--- /dev/null
+++ b/frontend/functions/ui/radio_group.tsx
@@ -0,0 +1,61 @@
+import * as React from "react"
+import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
+import { Radio } from "@base-ui/react/radio"
+import { cn } from "../core/cn"
+
+function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
+ return (
+
+ )
+}
+
+interface RadioGroupItemProps extends Radio.Root.Props {
+ label?: string
+ className?: string
+ labelClassName?: string
+}
+
+function RadioGroupItem({ className, label, id, labelClassName, ...props }: RadioGroupItemProps) {
+ const internalId = React.useId()
+ const itemId = id ?? internalId
+
+ return (
+
+
+
+
+
+
+ {label && (
+
+ )}
+
+ )
+}
+
+export { RadioGroup, RadioGroupItem }
+export type { RadioGroupItemProps }
diff --git a/frontend/functions/ui/sheet.md b/frontend/functions/ui/sheet.md
new file mode 100644
index 00000000..194e19d5
--- /dev/null
+++ b/frontend/functions/ui/sheet.md
@@ -0,0 +1,71 @@
+---
+name: sheet
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Sheet(props: SheetProps): JSX.Element"
+description: "Panel lateral deslizante (drawer) accesible con variantes de lado y animaciones. Base-UI Dialog con posicionamiento lateral via CVA."
+tags: [sheet, drawer, panel, component, ui, interactive, overlay, base-ui, cva]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["@base-ui/react/dialog", "class-variance-authority", "lucide-react"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/sheet.tsx"
+props:
+ - name: side
+ type: "'top' | 'bottom' | 'left' | 'right'"
+ required: false
+ description: "Lado desde el que aparece el panel (default: right)"
+ - name: showCloseButton
+ type: "boolean"
+ required: false
+ description: "Muestra el boton de cierre (default: true)"
+ - name: open
+ type: "boolean"
+ required: false
+ description: "Estado controlado de apertura"
+ - name: onOpenChange
+ type: "(open: boolean) => void"
+ required: false
+ description: "Callback cuando cambia el estado"
+emits: [onOpenChange]
+has_state: false
+framework: react
+variant: [top, bottom, left, right]
+---
+
+## Ejemplo
+
+```tsx
+
+
+
+
+
+
+ Editar perfil
+ Realiza cambios en tu perfil.
+
+
+ {/* contenido del panel */}
+
+
+
+
+
+
+
+
+
+```
+
+## Notas
+
+Reutiliza Base-UI Dialog para el comportamiento modal. Las animaciones de deslizamiento usan slide-in-from-* de Tailwind. CVA gestiona las variantes de lado. Exports: Sheet, SheetTrigger, SheetContent, SheetClose, SheetPortal, SheetOverlay, SheetHeader, SheetFooter, SheetTitle, SheetDescription.
diff --git a/frontend/functions/ui/sheet.tsx b/frontend/functions/ui/sheet.tsx
new file mode 100644
index 00000000..cd0b9f36
--- /dev/null
+++ b/frontend/functions/ui/sheet.tsx
@@ -0,0 +1,118 @@
+import * as React from "react"
+import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { XIcon } from "lucide-react"
+import { cn } from "../core/cn"
+
+function Sheet({ ...props }: DialogPrimitive.Root.Props) {
+ return
+}
+
+function SheetTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
+ return
+}
+
+function SheetClose({ ...props }: DialogPrimitive.Close.Props) {
+ return
+}
+
+function SheetPortal({ ...props }: DialogPrimitive.Portal.Props) {
+ return
+}
+
+function SheetOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
+ return (
+
+ )
+}
+
+const sheetVariants = cva(
+ "fixed z-50 flex flex-col gap-4 bg-background p-6 shadow-lg transition ease-in-out",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-open:animate-in data-open:slide-in-from-top data-closed:animate-out data-closed:slide-out-to-top",
+ bottom: "inset-x-0 bottom-0 border-t data-open:animate-in data-open:slide-in-from-bottom data-closed:animate-out data-closed:slide-out-to-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-open:animate-in data-open:slide-in-from-left data-closed:animate-out data-closed:slide-out-to-left sm:max-w-sm",
+ right: "inset-y-0 right-0 h-full w-3/4 border-l data-open:animate-in data-open:slide-in-from-right data-closed:animate-out data-closed:slide-out-to-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: { side: "right" },
+ }
+)
+
+interface SheetContentProps
+ extends DialogPrimitive.Popup.Props,
+ VariantProps {
+ showCloseButton?: boolean
+}
+
+function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }: SheetContentProps) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetTitle({ className, ...props }: DialogPrimitive.Title.Props) {
+ return (
+
+ )
+}
+
+function SheetDescription({ className, ...props }: DialogPrimitive.Description.Props) {
+ return (
+
+ )
+}
+
+export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, sheetVariants }
+export type { SheetContentProps }
diff --git a/frontend/functions/ui/simple_select.md b/frontend/functions/ui/simple_select.md
new file mode 100644
index 00000000..e366194a
--- /dev/null
+++ b/frontend/functions/ui/simple_select.md
@@ -0,0 +1,82 @@
+---
+name: simple_select
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "SimpleSelect(props: SimpleSelectProps): JSX.Element"
+description: "Select simplificado que acepta un array plano o agrupado de opciones. Wrapper sobre Select con API declarativa."
+tags: [select, dropdown, form, component, ui, simple]
+uses_functions: [cn_ts_core, select_ts_ui]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: [react]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/simple_select.tsx"
+props:
+ - name: value
+ type: "string"
+ required: true
+ description: "Valor seleccionado actualmente"
+ - name: onValueChange
+ type: "(value: string) => void"
+ required: true
+ description: "Callback cuando cambia la seleccion"
+ - name: options
+ type: "SimpleSelectOption[] | SimpleSelectGroup[]"
+ required: true
+ description: "Opciones planas o agrupadas"
+ - name: placeholder
+ type: "string"
+ required: false
+ description: "Texto cuando no hay seleccion"
+ - name: disabled
+ type: "boolean"
+ required: false
+ description: "Deshabilita el select"
+ - name: size
+ type: "'sm' | 'default'"
+ required: false
+ description: "Tamano del trigger"
+ - name: className
+ type: "string"
+ required: false
+ description: "Clases CSS adicionales"
+emits: []
+has_state: false
+framework: react
+variant: [default, sm]
+---
+
+## Ejemplo
+
+```tsx
+import { SimpleSelect } from '@fn_library'
+
+// Opciones planas
+const options = [
+ { value: 'a', label: 'Opcion A' },
+ { value: 'b', label: 'Opcion B' },
+]
+
+
+
+// Opciones agrupadas
+const grouped = [
+ { group: 'Frutas', items: [{ value: 'apple', label: 'Manzana' }] },
+ { group: 'Verduras', items: [{ value: 'carrot', label: 'Zanahoria' }] },
+]
+
+
+```
+
+## Notas
+
+- Detecta automaticamente si las opciones son planas o agrupadas via type guard `isGrouped`.
+- Wrapper sobre `Select` del registry — toda la logica de Base-UI y accesibilidad viene del componente base.
+- Soporta items deshabilitados individualmente con `disabled: true` en cada opcion.
diff --git a/frontend/functions/ui/simple_select.tsx b/frontend/functions/ui/simple_select.tsx
new file mode 100644
index 00000000..a9f1695d
--- /dev/null
+++ b/frontend/functions/ui/simple_select.tsx
@@ -0,0 +1,84 @@
+"use client"
+
+import * as React from "react"
+import { cn } from "../core/cn"
+import {
+ Select,
+ SelectTrigger,
+ SelectValue,
+ SelectContent,
+ SelectItem,
+ SelectGroup,
+ SelectGroupLabel,
+} from "./select"
+
+export interface SimpleSelectOption {
+ value: string
+ label: string
+ disabled?: boolean
+}
+
+export interface SimpleSelectGroup {
+ group: string
+ items: SimpleSelectOption[]
+}
+
+export type SimpleSelectOptions = SimpleSelectOption[] | SimpleSelectGroup[]
+
+interface SimpleSelectProps {
+ value: string
+ onValueChange: (value: string) => void
+ options: SimpleSelectOptions
+ placeholder?: string
+ disabled?: boolean
+ size?: 'sm' | 'default'
+ className?: string
+}
+
+function isGrouped(options: SimpleSelectOptions): options is SimpleSelectGroup[] {
+ return options.length > 0 && 'group' in options[0]
+}
+
+function SimpleSelect({
+ value,
+ onValueChange,
+ options,
+ placeholder = "Select...",
+ disabled = false,
+ size = 'default',
+ className,
+}: SimpleSelectProps) {
+ return (
+
+ )
+}
+
+export { SimpleSelect }
diff --git a/frontend/functions/ui/switch_toggle.md b/frontend/functions/ui/switch_toggle.md
new file mode 100644
index 00000000..539cae3d
--- /dev/null
+++ b/frontend/functions/ui/switch_toggle.md
@@ -0,0 +1,67 @@
+---
+name: switch_toggle
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "SwitchToggle(props: SwitchToggleProps): JSX.Element"
+description: "Toggle on/off accesible con label opcional a izquierda o derecha. Base-UI Switch primitive."
+tags: [switch, toggle, component, ui, interactive, form, base-ui]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: ["@base-ui/react/switch"]
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/switch_toggle.tsx"
+props:
+ - name: label
+ type: "string"
+ required: false
+ description: "Texto de etiqueta visible junto al switch"
+ - name: labelPosition
+ type: "'left' | 'right'"
+ required: false
+ description: "Posicion del label respecto al switch (default: right)"
+ - name: checked
+ type: "boolean"
+ required: false
+ description: "Estado controlado del toggle"
+ - name: defaultChecked
+ type: "boolean"
+ required: false
+ description: "Estado inicial no controlado"
+ - name: disabled
+ type: "boolean"
+ required: false
+ description: "Deshabilita el toggle"
+ - name: onCheckedChange
+ type: "(checked: boolean) => void"
+ required: false
+ description: "Callback cuando cambia el estado"
+emits: [onCheckedChange]
+has_state: false
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+// Label a la derecha (default)
+
+
+// Label a la izquierda
+
+
+// Solo switch sin label
+
+```
+
+## Notas
+
+Usa Base-UI Switch primitive. El thumb se traslada con translate-x via Tailwind. El id se genera con useId si no se provee para conectar el label.
diff --git a/frontend/functions/ui/switch_toggle.tsx b/frontend/functions/ui/switch_toggle.tsx
new file mode 100644
index 00000000..7a2a9681
--- /dev/null
+++ b/frontend/functions/ui/switch_toggle.tsx
@@ -0,0 +1,66 @@
+import * as React from "react"
+import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
+import { cn } from "../core/cn"
+
+interface SwitchToggleProps extends SwitchPrimitive.Root.Props {
+ label?: string
+ labelPosition?: "left" | "right"
+ className?: string
+}
+
+function SwitchToggle({ className, label, labelPosition = "right", id, ...props }: SwitchToggleProps) {
+ const internalId = React.useId()
+ const switchId = id ?? internalId
+
+ const switchEl = (
+
+
+
+ )
+
+ if (!label) return switchEl
+
+ return (
+
+ {labelPosition === "left" && (
+
+ )}
+ {switchEl}
+ {labelPosition === "right" && (
+
+ )}
+
+ )
+}
+
+export { SwitchToggle }
+export type { SwitchToggleProps }
diff --git a/frontend/functions/ui/textarea.md b/frontend/functions/ui/textarea.md
new file mode 100644
index 00000000..57d37928
--- /dev/null
+++ b/frontend/functions/ui/textarea.md
@@ -0,0 +1,66 @@
+---
+name: textarea
+kind: component
+lang: ts
+domain: ui
+version: "1.0.0"
+purity: impure
+signature: "Textarea(props: TextareaProps): JSX.Element"
+description: "Input multilinea accesible con auto-resize opcional. Patron identico a Input para consistencia de estilos."
+tags: [textarea, component, ui, interactive, form]
+uses_functions: [cn_ts_core]
+uses_types: []
+returns: []
+returns_optional: false
+error_type: ""
+imports: []
+tested: false
+tests: []
+test_file_path: ""
+file_path: "frontend/functions/ui/textarea.tsx"
+props:
+ - name: autoResize
+ type: "boolean"
+ required: false
+ description: "Ajusta la altura automaticamente al contenido (default: false)"
+ - name: placeholder
+ type: "string"
+ required: false
+ description: "Texto placeholder"
+ - name: disabled
+ type: "boolean"
+ required: false
+ description: "Deshabilita el textarea"
+ - name: rows
+ type: "number"
+ required: false
+ description: "Numero de filas visibles iniciales"
+ - name: className
+ type: "string"
+ required: false
+ description: "Clases CSS adicionales"
+emits: [onChange, onFocus, onBlur]
+has_state: true
+framework: react
+variant: []
+---
+
+## Ejemplo
+
+```tsx
+// Basico
+
+
+// Con auto-resize
+
+
+// Controlado
+