# functions

id: wails_bind_crud_go_infra
name: wails_bind_crud
kind: function
lang: go
domain: infra
version: 1.0.0
purity: pure
signature: GenerateWailsCRUD(spec WailsCRUDSpec) string
tags: ["wails","crud","generator","bindings","codegen","infra"]
uses_functions: []
uses_types: []
returns: []
imports: ["fmt","strings"]
tests: []
file_path: functions/infra/wails_bind_crud.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 021352757307ee8e1012c7a7e7f3ed164224bb5a41d3d56a01c00ac71d8acdf5
description:
  Genera código Go de bindings CRUD para Wails: struct + métodos List/Get/Create/Update/Delete con stubs not-implemented.

example:
  ```go
  code := GenerateWailsCRUD(WailsCRUDSpec{
      EntityName: "User",
      Fields:     []string{"Name string", "Email string", "Role string"},
      WithList:   true,
      WithGet:    true,
      WithCreate: true,
      WithUpdate: true,
      WithDelete: true,
  })
  // Genera: type User struct + ListUsers + GetUser + CreateUser + UpdateUser + DeleteUser
  ```

notes:
  Función pura — genera código como string. Los métodos son stubs con `return fmt.Errorf("not implemented")`. Un agente puede generar el código, insertarlo en app.go, y luego implementar los TODOs.

code:
  package infra

  import (
  	"fmt"
  	"strings"
  )

  // WailsCRUDSpec define la especificación para generar bindings CRUD.
  type WailsCRUDSpec struct {
  	EntityName string   // Nombre de la entidad (PascalCase, ej: "User")
  	Fields     []string // Campos de la entidad (ej: ["Name string", "Email string"])
  	WithList   bool     // Generar List method
  	WithGet    bool     // Generar Get method
  	WithCreate bool     // Generar Create method
  	WithUpdate bool     // Generar Update method
  	WithDelete bool     // Generar Delete method
  }

  // GenerateWailsCRUD genera código Go de bindings CRUD para una entidad.
  // Retorna el código Go como string — función pura.
  func GenerateWailsCRUD(spec WailsCRUDSpec) string {
  	var sb strings.Builder
  	lower := strings.ToLower(spec.EntityName)
  	plural := lower + "s"

  	// Tipo
  	sb.WriteString(fmt.Sprintf("// %s entity\n", spec.EntityName))
  	sb.WriteString(fmt.Sprintf("type %s struct {\n", spec.EntityName))
  	sb.WriteString("\tID string `json:\"id\"`\n")
  	for _, field := range spec.Fields {
  		parts := strings.SplitN(field, " ", 2)
  		if len(parts) == 2 {
  			jsonTag := strings.ToLower(parts[0])
  			sb.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\"`\n", parts[0], parts[1], jsonTag))
  		}
  	}
  	sb.WriteString("}\n\n")

  	// List
  	if spec.WithList {
  		sb.WriteString(fmt.Sprintf("// List%s retorna todos los %s.\n", spec.EntityName+"s", plural))
  		sb.WriteString(fmt.Sprintf("func (a *App) List%s() ([]%s, error) {\n", spec.EntityName+"s", spec.EntityName))
  		sb.WriteString(fmt.Sprintf("\t// TODO: implement List%s\n", spec.EntityName+"s"))
  		sb.WriteString(fmt.Sprintf("\treturn nil, fmt.Errorf(\"not implemented\")\n"))
  		sb.WriteString("}\n\n")
  	}

  	// Get
  	if spec.WithGet {
  		sb.WriteString(fmt.Sprintf("// Get%s retorna un %s por ID.\n", spec.EntityName, lower))
  		sb.WriteString(fmt.Sprintf("func (a *App) Get%s(id string) (%s, error) {\n", spec.EntityName, spec.EntityName))
  		sb.WriteString(fmt.Sprintf("\t// TODO: implement Get%s\n", spec.EntityName))
  		sb.WriteString(fmt.Sprintf("\treturn %s{}, fmt.Errorf(\"not implemented\")\n", spec.EntityName))
  		sb.WriteString("}\n\n")
  	}

  	// Create
  	if spec.WithCreate {
  		sb.WriteString(fmt.Sprintf("// Create%s crea un nuevo %s.\n", spec.EntityName, lower))
  		sb.WriteString(fmt.Sprintf("func (a *App) Create%s(%s %s) (%s, error) {\n", spec.EntityName, lower, spec.EntityName, spec.EntityName))
  		sb.WriteString(fmt.Sprintf("\t// TODO: implement Create%s\n", spec.EntityName))
  		sb.WriteString(fmt.Sprintf("\treturn %s, fmt.Errorf(\"not implemented\")\n", lower))
  		sb.WriteString("}\n\n")
  	}

  	// Update
  	if spec.WithUpdate {
  		sb.WriteString(fmt.Sprintf("// Update%s actualiza un %s existente.\n", spec.EntityName, lower))
  		sb.WriteString(fmt.Sprintf("func (a *App) Update%s(%s %s) (%s, error) {\n", spec.EntityName, lower, spec.EntityName, spec.EntityName))
  		sb.WriteString(fmt.Sprintf("\t// TODO: implement Update%s\n", spec.EntityName))
  		sb.WriteString(fmt.Sprintf("\treturn %s, fmt.Errorf(\"not implemented\")\n", lower))
  		sb.WriteString("}\n\n")
  	}

  	// Delete
  	if spec.WithDelete {
  		sb.WriteString(fmt.Sprintf("// Delete%s elimina un %s por ID.\n", spec.EntityName, lower))
  		sb.WriteString(fmt.Sprintf("func (a *App) Delete%s(id string) error {\n", spec.EntityName))
  		sb.WriteString(fmt.Sprintf("\t// TODO: implement Delete%s\n", spec.EntityName))
  		sb.WriteString("\treturn fmt.Errorf(\"not implemented\")\n")
  		sb.WriteString("}\n\n")
  	}

  	return sb.String()
  }

params_schema:
  {"params":[{"name":"spec","desc":"estructura WailsCRUDSpec con EntityName, Fields, WithList/Get/Create/Update/Delete flags"}],"output":"string con codigo Go generado (struct + metodos CRUD stubs)"}


id: wails_build_go_infra
name: wails_build
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: WailsBuild(ctx context.Context, cfg WailsBuildConfig) error
tags: ["wails","build","compile","desktop","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["os/exec","fmt","strings","context"]
tests: []
file_path: functions/infra/wails_build.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Requiere `wails` CLI instalado. WailsDev bloquea el proceso (es un servidor de desarrollo).
content_hash: 29867491260074c6a0c1bad048b1278cfd099a503463ac4fa26f4c0f30f3e703
description:
  Compila un proyecto Wails para linux/windows/darwin. Incluye WailsDev para modo desarrollo con hot reload.

example:
  ```go
  // Build para linux
  WailsBuild(ctx, WailsBuildConfig{
      Dir:      "/home/user/my-app",
      Platform: "linux",
  })

  // Desarrollo con hot reload + browser
  WailsDev(ctx, "/home/user/my-app", true)
  ```

code:
  package infra

  import (
  	"context"
  	"fmt"
  	"os/exec"
  	"strings"
  )

  // WailsBuildConfig configura la compilación de un proyecto Wails.
  type WailsBuildConfig struct {
  	Dir      string // Directorio del proyecto Wails
  	Platform string // linux, windows, darwin (default: current)
  	Output   string // Nombre del binario de salida
  	Debug    bool   // Build con debug info
  }

  // WailsBuild compila un proyecto Wails para la plataforma especificada.
  func WailsBuild(ctx context.Context, cfg WailsBuildConfig) error {
  	if cfg.Dir == "" {
  		return fmt.Errorf("dir is required")
  	}

  	args := []string{"build"}

  	if cfg.Platform != "" {
  		args = append(args, "-platform", cfg.Platform)
  	}
  	if cfg.Output != "" {
  		args = append(args, "-o", cfg.Output)
  	}
  	if cfg.Debug {
  		args = append(args, "-debug")
  	}

  	cmd := exec.CommandContext(ctx, "wails", args...)
  	cmd.Dir = cfg.Dir

  	output, err := cmd.CombinedOutput()
  	if err != nil {
  		return fmt.Errorf("wails build failed: %w\n%s", err, string(output))
  	}

  	fmt.Printf("Built successfully: %s\n", strings.TrimSpace(string(output)))
  	return nil
  }

  // WailsDev lanza un proyecto Wails en modo desarrollo con hot reload.
  func WailsDev(ctx context.Context, dir string, browser bool) error {
  	if dir == "" {
  		return fmt.Errorf("dir is required")
  	}

  	args := []string{"dev"}
  	if browser {
  		args = append(args, "-browser")
  	}

  	cmd := exec.CommandContext(ctx, "wails", args...)
  	cmd.Dir = dir
  	cmd.Stdout = nil // inherit
  	cmd.Stderr = nil

  	return cmd.Run()
  }

params_schema:
  {"params":[{"name":"ctx","desc":"context para cancelacion"},{"name":"cfg","desc":"estructura WailsBuildConfig con Dir, Platform (linux/windows/darwin)"}],"output":"nil si la compilacion fue exitosa, error en caso contrario"}


id: wails_emit_event_go_infra
name: wails_emit_event
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: WailsEmitEvent(ctx context.Context, eventName string, data interface{})
tags: ["wails","event","emit","ipc","realtime","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["context","encoding/json","fmt","time","github.com/wailsapp/wails/v2/pkg/runtime"]
tests: []
file_path: functions/infra/wails_emit_event.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7f4a1a50609433623d66dbb19bd9ab8060876d0fa5456170cd4f4fad3a6ee74d
description:
  Emite eventos tipados de Go al frontend con timestamp automático. Incluye WailsEmitJSON para serialización explícita.

example:
  ```go
  // Emitir evento tipado
  WailsEmitEvent(ctx, "price:update", PriceData{Symbol: "BTC", Price: 67500.0})

  // El frontend lo recibe con useWailsEvent:
  // useWailsEvent({ eventName: 'price:update', onEvent: (data) => ... })
  ```

notes:
  El evento llega al frontend como WailsEventPayload con `type`, `data` y `timestamp`. Complementa `use_wails_event` del lado TS. Requiere la dependencia `github.com/wailsapp/wails/v2` en el proyecto destino.

code:
  //go:build ignore
  // NOTE: requires wails dependency in target project — use this file by copying into a Wails project

  package infra

  import (
  	"context"
  	"encoding/json"
  	"fmt"
  	"time"

  	"github.com/wailsapp/wails/v2/pkg/runtime"
  )

  // WailsEventPayload es un envelope para eventos tipados.
  type WailsEventPayload struct {
  	Type      string      `json:"type"`
  	Data      interface{} `json:"data"`
  	Timestamp time.Time   `json:"timestamp"`
  }

  // WailsEmitEvent emite un evento tipado al frontend.
  func WailsEmitEvent(ctx context.Context, eventName string, data interface{}) {
  	payload := WailsEventPayload{
  		Type:      eventName,
  		Data:      data,
  		Timestamp: time.Now(),
  	}
  	runtime.EventsEmit(ctx, eventName, payload)
  }

  // WailsEmitJSON emite datos como JSON string al frontend.
  func WailsEmitJSON(ctx context.Context, eventName string, data interface{}) error {
  	b, err := json.Marshal(data)
  	if err != nil {
  		return fmt.Errorf("marshaling event data: %w", err)
  	}
  	runtime.EventsEmit(ctx, eventName, string(b))
  	return nil
  }

params_schema:
  {"params":[{"name":"ctx","desc":"context de Wails"},{"name":"eventName","desc":"nombre del evento a emitir (ej: price:update)"},{"name":"data","desc":"datos a serializar y enviar al frontend"}],"output":"void (sin valor de retorno, efecto es emitir el evento)"}


id: wails_stream_data_go_infra
name: wails_stream_data
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: WailsStreamData[T any](ctx context.Context, cfg WailsStreamConfig, data []T) error
tags: ["wails","stream","ipc","realtime","chunks","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: ["context","fmt","time","github.com/wailsapp/wails/v2/pkg/runtime"]
tests: []
file_path: functions/infra/wails_stream_data.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 9e95a95081a919411bb80efa578abead56b149c821158ad68f847377ce859ae3
description:
  Envía datos como stream Go→TS con protocolo {name}/{name}:complete/{name}:error. Incluye WailsStreamFunc para generadores.

example:
  ```go
  // Stream de slice
  WailsStreamData(ctx, WailsStreamConfig{
      StreamName: "logs",
      ChunkDelay: 10 * time.Millisecond,
  }, logLines)

  // Stream con generador
  WailsStreamFunc(ctx, "metrics", func(ctx context.Context, ch chan<- Metric) error {
      for {
          select {
          case <-ctx.Done():
              return nil
          case <-time.After(time.Second):
              ch <- collectMetric()
          }
      }
  })

  // Frontend lo recibe con useWailsStream:
  // useWailsStream({ streamName: 'metrics', autoStart: true })
  ```

notes:
  Protocolo: chunks en `{streamName}`, fin en `{streamName}:complete`, error en `{streamName}:error`. Compatible con `use_wails_stream` del lado TS. WailsStreamFunc usa goroutine + canal para datos asíncronos. Requiere Go 1.18+ (generics) y la dependencia `github.com/wailsapp/wails/v2` en el proyecto destino.

code:
  //go:build ignore
  // NOTE: requires wails dependency in target project — use this file by copying into a Wails project

  package infra

  import (
  	"context"
  	"fmt"
  	"time"

  	"github.com/wailsapp/wails/v2/pkg/runtime"
  )

  // WailsStreamConfig configura un stream de datos hacia el frontend.
  type WailsStreamConfig struct {
  	StreamName string        // Nombre del stream (evento base)
  	ChunkDelay time.Duration // Delay entre chunks (default 0)
  }

  // WailsStreamData envía un slice de datos como stream al frontend.
  // Emite cada elemento como chunk en {streamName}, y {streamName}:complete al terminar.
  func WailsStreamData[T any](ctx context.Context, cfg WailsStreamConfig, data []T) error {
  	if cfg.StreamName == "" {
  		return fmt.Errorf("stream name is required")
  	}

  	for _, chunk := range data {
  		select {
  		case <-ctx.Done():
  			runtime.EventsEmit(ctx, cfg.StreamName+":error", map[string]string{
  				"message": "stream cancelled",
  			})
  			return ctx.Err()
  		default:
  			runtime.EventsEmit(ctx, cfg.StreamName, chunk)
  			if cfg.ChunkDelay > 0 {
  				time.Sleep(cfg.ChunkDelay)
  			}
  		}
  	}

  	runtime.EventsEmit(ctx, cfg.StreamName+":complete", nil)
  	return nil
  }

  // WailsStreamFunc ejecuta una función generadora y envía resultados como stream.
  // La función generadora envía datos por el canal, y esta función los retransmite al frontend.
  func WailsStreamFunc[T any](ctx context.Context, streamName string, generator func(ctx context.Context, ch chan<- T) error) error {
  	ch := make(chan T, 100)
  	errCh := make(chan error, 1)

  	go func() {
  		defer close(ch)
  		errCh <- generator(ctx, ch)
  	}()

  	for chunk := range ch {
  		runtime.EventsEmit(ctx, streamName, chunk)
  	}

  	if err := <-errCh; err != nil {
  		runtime.EventsEmit(ctx, streamName+":error", map[string]string{
  			"message": err.Error(),
  		})
  		return err
  	}

  	runtime.EventsEmit(ctx, streamName+":complete", nil)
  	return nil
  }

params_schema:
  {"params":[{"name":"ctx","desc":"context de Wails"},{"name":"cfg","desc":"estructura WailsStreamConfig con StreamName y ChunkDelay"},{"name":"data","desc":"slice de datos genericos a enviar en chunks"}],"output":"nil si el stream se completo exitosamente, error en caso contrario"}


id: write_dockerfile_go_infra
name: write_dockerfile
kind: function
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func WriteDockerfile(dir, content string) (string, error)
tags: ["docker","dockerfile","io","write","deploy","infra"]
uses_functions: ["generate_dockerfile_go_infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: ["fmt","os","path/filepath"]
test_file_path: functions/infra/write_dockerfile_test.go
file_path: functions/infra/write_dockerfile.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d2b96af570f088cb1ea5aeaf933ba61a5bfee8ccb2f0c08e7b91a6cc72c4aa0d
description:
  Escribe content en dir/Dockerfile. Crea el directorio si no existe. Retorna el path absoluto del archivo escrito. Compañera impura de generate_dockerfile.

example:
  ```go
  // Patron puro+impuro: generar contenido y luego escribir
  content := GenerateDockerfile("myapp", 8080, map[string]string{"PORT": "8080"})
  path, err := WriteDockerfile("/home/user/apps/myapp", content)
  if err != nil {
      log.Fatal(err)
  }
  fmt.Println("Dockerfile escrito en:", path)
  // /home/user/apps/myapp/Dockerfile
  ```

tests:
  ["escribe Dockerfile en directorio existente","crea directorio si no existe","retorna path absoluto correcto","error si dir es path invalido"]

notes:
  Patron puro+impuro: generate_dockerfile produce el texto (pura, testeable sin I/O), write_dockerfile lo persiste (impura, efecto secundario aislado). Esto facilita testear la generacion del contenido independientemente de la escritura. Sobreescribe cualquier Dockerfile existente en el directorio.

code:
  package infra

  import (
  	"fmt"
  	"os"
  	"path/filepath"
  )

  // WriteDockerfile escribe content en dir/Dockerfile.
  // Crea el directorio si no existe. Retorna el path absoluto del archivo escrito.
  func WriteDockerfile(dir, content string) (string, error) {
  	absDir, err := filepath.Abs(dir)
  	if err != nil {
  		return "", fmt.Errorf("write_dockerfile: resolver path %s: %w", dir, err)
  	}

  	if err := os.MkdirAll(absDir, 0o755); err != nil {
  		return "", fmt.Errorf("write_dockerfile: crear directorio %s: %w", absDir, err)
  	}

  	dockerfilePath := filepath.Join(absDir, "Dockerfile")
  	if err := os.WriteFile(dockerfilePath, []byte(content), 0o644); err != nil {
  		return "", fmt.Errorf("write_dockerfile: escribir %s: %w", dockerfilePath, err)
  	}

  	return dockerfilePath, nil
  }

params_schema:
  {"params":[{"name":"dir","desc":"directorio donde crear el archivo Dockerfile (se crea si no existe)"},{"name":"content","desc":"contenido del Dockerfile como string"}],"output":"ruta absoluta del archivo Dockerfile escrito"}


id: docker_tui_go_infra
name: docker_tui
kind: pipeline
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func main() — TUI fullscreen para gestionar Docker
tags: ["docker","tui","pipeline","infra","bubbletea","devfactory"]
returns: []
error_type: error_go_core
imports: ["github.com/charmbracelet/bubbletea","github.com/lucasdataproyects/devfactory"]
example:
  ```bash
  cd apps/docker_tui && go run .
  ```

tests: []
file_path: apps/docker_tui/main.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: a648b5553e92e62da99149111d1f2a892733b254a8ca6317b3ee5371767e68df
description:
  Pipeline que compone componentes TUI de DevFactory con comandos Docker para crear una aplicacion de terminal interactiva. Gestiona containers, images, volumes, networks y compose.

uses_functions:
  ["new_filtered_list_go_tui","new_list_go_tui","new_spinner_go_tui","new_base_model_go_tui","default_styles_go_tui","run_fullscreen_go_tui","run_cmd_timeout_go_shell","docker_list_containers_go_infra","docker_start_container_go_infra","docker_stop_container_go_infra","docker_container_logs_go_infra","docker_list_images_go_infra","docker_remove_image_go_infra"]

uses_types:
  ["container_go_docker","image_go_docker","volume_go_docker","network_go_docker","compose_project_go_docker","list_model_go_tui","filtered_list_model_go_tui","spinner_model_go_tui","styles_go_tui","base_model_go_tui","cmd_result_go_shell"]

notes:
  Pipeline principal del proyecto Docker TUI. Compone funciones de los dominios tui, shell e infra
  para crear una aplicacion de terminal con 5 vistas: Containers (start/stop/restart/logs),
  Images (list/remove), Volumes (list/remove), Networks (list/remove), Compose (up/down/logs).

  Arquitectura Elm (Model-Update-View) con navegacion por tabs y state machines por vista.

code:
  package main

  import (
  	"fmt"
  	"os"

  	"docker-tui/app"

  	"github.com/lucasdataproyects/devfactory/tui"
  )

  func main() {
  	model := app.New()
  	result := tui.RunFullscreen(model)

  	if result.IsErr() {
  		fmt.Fprintf(os.Stderr, "error: %v\n", result.Error())
  		os.Exit(1)
  	}
  }

params_schema:
  {"output":"Ejecutable TUI fullscreen que gestiona Docker containers, images, volumes, networks y compose"}


id: init_metabase_go_infra
name: init_metabase
kind: pipeline
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func main() — Despliega stack Metabase + Postgres en Docker
tags: ["docker","metabase","postgres","pipeline","infra","analytics","launcher"]
uses_types: ["container_info_go_infra","network_go_docker"]
returns: []
error_type: error_go_core
imports: ["os/exec","encoding/json"]
tests: []
file_path: functions/pipelines/init_metabase/main.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:44:15Z
props: []
emits: []
variant: []
content_hash: 11062eda7f4a4c138360635c05232d7271d7624bf0742e2923c642c126376860
params_schema: {"output":"stdout: JSON con IDs de red, contenedores (postgres, metabase) y URL de acceso"}
description:
  Pipeline que inicializa un contenedor Metabase con su base de datos Postgres. Crea red Docker, pull de imágenes, inicia Postgres con volume persistente, espera health check y lanza Metabase conectado.

uses_functions:
  ["docker_create_network_go_infra","docker_pull_image_go_infra","docker_run_container_go_infra","docker_inspect_container_go_infra","retry_with_backoff_go_core"]

example:
  ```bash
  # Básico
  go run functions/pipelines/init_metabase/main.go \
    --project analytics \
    --metabase-port 3000 \
    --pg-user metabase \
    --pg-password metabase

  # Con volume para registry.db (detecta cambios en vivo)
  go run functions/pipelines/init_metabase/main.go \
    --project fn_registry \
    --mb-volumes "/home/lucas/fn_registry/registry.db:/data/registry.db"
  ```

  Salida JSON:
  ```json
  {
    "network_id": "abc123...",
    "postgres_id": "def456...",
    "metabase_id": "ghi789...",
    "network_name": "analytics-net",
    "postgres_name": "analytics-postgres",
    "metabase_name": "analytics-metabase",
    "metabase_url": "http://localhost:3000"
  }
  ```

notes:
  El pipeline orquesta 5 pasos secuenciales:

  1. **Red Docker** — crea `{project}-net` con driver bridge
  2. **Pull** — descarga `postgres:16` y `metabase/metabase:latest`
  3. **Postgres** — inicia con volume persistente (named volume por defecto o bind mount con `--pg-volume`)
  4. **Health check** — retry exponencial (hasta ~34 min) con `pg_isready` dentro del contenedor
  5. **Metabase** — conecta a Postgres via red interna, expone en puerto configurable. Con `--mb-volumes` monta volumes adicionales (ej: registry.db para SQLite)
  6. **Permisos** — ajusta ownership de directorios montados para el usuario `metabase` (UID 2000) dentro del contenedor

  Reutiliza conceptualmente `docker_create_network`, `docker_pull_image`, `docker_run_container`, `docker_inspect_container` y `retry_with_backoff`, reimplementadas inline por ser un ejecutable independiente.

  Para destruir el stack:
  ```bash
  docker stop analytics-metabase analytics-postgres
  docker rm analytics-metabase analytics-postgres
  docker network rm analytics-net
  ```

code:
  package main

  import (
  	"encoding/json"
  	"flag"
  	"fmt"
  	"os"
  	"os/exec"
  	"strings"
  	"time"
  )

  // MetabaseResult contiene los IDs y nombres de los recursos creados.
  type MetabaseResult struct {
  	NetworkID    string `json:"network_id"`
  	PostgresID   string `json:"postgres_id"`
  	MetabaseID   string `json:"metabase_id"`
  	NetworkName  string `json:"network_name"`
  	PostgresName string `json:"postgres_name"`
  	MetabaseName string `json:"metabase_name"`
  	MetabaseURL  string `json:"metabase_url"`
  }

  func main() {
  	project := flag.String("project", "", "Prefijo para nombres de contenedores y red (requerido)")
  	mbPort := flag.String("metabase-port", "3000", "Puerto host para Metabase")
  	pgPort := flag.String("pg-port", "5432", "Puerto host para Postgres")
  	pgUser := flag.String("pg-user", "metabase", "Usuario Postgres")
  	pgPass := flag.String("pg-password", "metabase", "Password Postgres")
  	pgDB := flag.String("pg-database", "metabase", "Base de datos Postgres")
  	pgVolume := flag.String("pg-volume", "", "Path host para persistencia Postgres (default: docker named volume)")
  	mbVolumes := flag.String("mb-volumes", "", "Volumes adicionales para Metabase, separados por coma (ej: /host/path:/container/path,/otro:/dest)")
  	flag.Parse()

  	if *project == "" {
  		fmt.Fprintln(os.Stderr, "error: --project requerido")
  		flag.Usage()
  		os.Exit(1)
  	}

  	var extraVolumes []string
  	if *mbVolumes != "" {
  		extraVolumes = strings.Split(*mbVolumes, ",")
  	}

  	result, err := initMetabase(*project, *mbPort, *pgPort, *pgUser, *pgPass, *pgDB, *pgVolume, extraVolumes)
  	if err != nil {
  		fmt.Fprintf(os.Stderr, "error: %v\n", err)
  		os.Exit(1)
  	}

  	enc := json.NewEncoder(os.Stdout)
  	enc.SetIndent("", "  ")
  	enc.Encode(result)
  }

  func initMetabase(project, mbPort, pgPort, pgUser, pgPass, pgDB, pgVolume string, mbExtraVolumes []string) (*MetabaseResult, error) {
  	networkName := project + "-net"
  	pgName := project + "-postgres"
  	mbName := project + "-metabase"

  	// 1. Crear red
  	fmt.Fprintf(os.Stderr, "[1/5] Creando red %s...\n", networkName)
  	netID, err := dockerCreateNetwork(networkName, "bridge")
  	if err != nil {
  		return nil, fmt.Errorf("creating network: %w", err)
  	}

  	// 2. Pull imágenes
  	for i, img := range []string{"postgres:16", "metabase/metabase:latest"} {
  		fmt.Fprintf(os.Stderr, "[2/5] Pulling %s (%d/2)...\n", img, i+1)
  		if err := dockerPull(img); err != nil {
  			return nil, fmt.Errorf("pulling %s: %w", img, err)
  		}
  	}

  	// 3. Iniciar Postgres
  	fmt.Fprintf(os.Stderr, "[3/5] Iniciando Postgres %s...\n", pgName)
  	vol := pgName + "-data:/var/lib/postgresql/data"
  	if pgVolume != "" {
  		vol = pgVolume + ":/var/lib/postgresql/data"
  	}

  	pgArgs := []string{
  		"run", "-d",
  		"--name", pgName,
  		"--network", networkName,
  		"-p", pgPort + ":5432",
  		"-e", "POSTGRES_USER=" + pgUser,
  		"-e", "POSTGRES_PASSWORD=" + pgPass,
  		"-e", "POSTGRES_DB=" + pgDB,
  		"-v", vol,
  		"postgres:16",
  	}
  	pgID, err := dockerCmd(pgArgs...)
  	if err != nil {
  		return nil, fmt.Errorf("starting postgres: %w", err)
  	}

  	// 4. Health check con retry exponencial
  	fmt.Fprintf(os.Stderr, "[4/5] Esperando a que Postgres esté listo...\n")
  	err = retryWithBackoff(func() error {
  		out, execErr := dockerCmd("exec", pgName, "pg_isready", "-U", pgUser)
  		if execErr != nil {
  			return fmt.Errorf("pg_isready: %s", out)
  		}
  		return nil
  	}, 10, 2*time.Second)
  	if err != nil {
  		return nil, fmt.Errorf("waiting for postgres: %w", err)
  	}
  	fmt.Fprintf(os.Stderr, "       Postgres listo.\n")

  	// 5. Iniciar Metabase
  	fmt.Fprintf(os.Stderr, "[5/5] Iniciando Metabase %s...\n", mbName)
  	mbArgs := []string{
  		"run", "-d",
  		"--name", mbName,
  		"--network", networkName,
  		"-p", mbPort + ":3000",
  		"-e", "MB_DB_TYPE=postgres",
  		"-e", "MB_DB_DBNAME=" + pgDB,
  		"-e", "MB_DB_PORT=5432",
  		"-e", "MB_DB_USER=" + pgUser,
  		"-e", "MB_DB_PASS=" + pgPass,
  		"-e", "MB_DB_HOST=" + pgName,
  	}
  	for _, v := range mbExtraVolumes {
  		mbArgs = append(mbArgs, "-v", strings.TrimSpace(v))
  	}
  	mbArgs = append(mbArgs, "metabase/metabase:latest")
  	mbID, err := dockerCmd(mbArgs...)
  	if err != nil {
  		return nil, fmt.Errorf("starting metabase: %w", err)
  	}

  	// 6. Fix permisos para volumes SQLite — Metabase corre como UID 2000
  	for _, v := range mbExtraVolumes {
  		parts := strings.SplitN(strings.TrimSpace(v), ":", 2)
  		if len(parts) < 2 {
  			continue
  		}
  		destPath := parts[1]
  		// Si es un archivo, fix el directorio padre; si es directorio, fix directo
  		dir := destPath
  		if strings.Contains(destPath, ".") {
  			// Probablemente un archivo, usar dirname
  			idx := strings.LastIndex(destPath, "/")
  			if idx > 0 {
  				dir = destPath[:idx]
  			}
  		}
  		fmt.Fprintf(os.Stderr, "       Fijando permisos de %s para usuario metabase...\n", dir)
  		dockerCmd("exec", "-u", "root", mbName, "chown", "metabase:metabase", dir)
  	}

  	mbURL := fmt.Sprintf("http://localhost:%s", mbPort)
  	fmt.Fprintf(os.Stderr, "\nStack listo. Metabase disponible en %s\n", mbURL)

  	return &MetabaseResult{
  		NetworkID:    netID,
  		PostgresID:   pgID,
  		MetabaseID:   mbID,
  		NetworkName:  networkName,
  		PostgresName: pgName,
  		MetabaseName: mbName,
  		MetabaseURL:  mbURL,
  	}, nil
  }

  // --- Funciones Docker (reimplementan las del registry sin imports internos) ---

  func dockerCreateNetwork(name, driver string) (string, error) {
  	return dockerCmd("network", "create", "--driver", driver, name)
  }

  func dockerPull(image string) error {
  	_, err := dockerCmd("pull", image)
  	return err
  }

  func dockerCmd(args ...string) (string, error) {
  	out, err := exec.Command("docker", args...).CombinedOutput()
  	if err != nil {
  		return "", fmt.Errorf("docker %s: %s", args[0], strings.TrimSpace(string(out)))
  	}
  	return strings.TrimSpace(string(out)), nil
  }

  // retryWithBackoff reimplementa RetryWithBackoff de core para funciones () error.
  func retryWithBackoff(fn func() error, maxRetries int, baseDelay time.Duration) error {
  	var lastErr error
  	for attempt := 0; attempt <= maxRetries; attempt++ {
  		if err := fn(); err == nil {
  			return nil
  		} else {
  			lastErr = err
  		}
  		if attempt < maxRetries {
  			delay := baseDelay * (1 << uint(attempt))
  			time.Sleep(delay)
  		}
  	}
  	return fmt.Errorf("all %d retries exhausted: %w", maxRetries+1, lastErr)
  }


id: pipeline_launcher_go_infra
name: pipeline_launcher
kind: pipeline
lang: go
domain: infra
version: 1.0.0
purity: impure
signature: func main() — TUI fullscreen para lanzar pipelines y registrar ejecuciones
tags: ["tui","pipeline","infra","operations","bubbletea","devfactory"]
uses_functions: ["docker_tui_go_infra"]
uses_types: ["base_model_go_tui","filtered_list_model_go_tui","spinner_model_go_tui","styles_go_tui"]
returns: []
error_type: error_go_core
example:
  ```bash
  FN_REGISTRY_ROOT=/home/lucas/fn_registry go run apps/pipeline_launcher/main.go
  ```

tests: []
file_path: apps/pipeline_launcher/main.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 3ce494526fbad4b8832594a7f4cb089ee226f2b98d24a04a3e681a3736266e84
description:
  TUI interactiva que lista pipelines del registry, permite lanzarlos como subprocesos y registra cada ejecución en operations.db. Dos tabs: Pipelines (filtro + launch) y History (historial).

imports:
  ["github.com/charmbracelet/bubbletea","github.com/lucasdataproyects/devfactory","fn-registry/registry","fn-registry/fn_operations"]

notes:
  TUI con dos tabs:

  - **Pipelines**: lista filtrable de todos los pipelines del registry. Enter lanza el pipeline seleccionado como subproceso (`go run .`), muestra spinner durante la ejecución, y al terminar muestra stdout/stderr y registra la ejecución en operations.db.
  - **History**: historial de ejecuciones pasadas con status, duración y timestamp. Enter muestra detalle completo.

  Teclas: Tab/Shift+Tab cambia vista, Enter ejecuta acción, r refresca, / filtra, j/k scroll, Esc vuelve.

  Requiere `FN_REGISTRY_ROOT` apuntando a la raíz del fn-registry para encontrar registry.db y los file_path de los pipelines.

code:
  package main

  import (
  	"fmt"
  	"os"

  	"pipeline-launcher/app"
  	"pipeline-launcher/config"

  	"github.com/lucasdataproyects/devfactory/tui"
  )

  func main() {
  	cfg := config.Default()

  	model, err := app.New(cfg)
  	if err != nil {
  		fmt.Fprintf(os.Stderr, "error: %v\n", err)
  		os.Exit(1)
  	}
  	defer model.Close()

  	result := tui.RunFullscreen(model)

  	if result.IsErr() {
  		fmt.Fprintf(os.Stderr, "error: %v\n", result.Error())
  		os.Exit(1)
  	}
  }

params_schema:
  {"output":"Ejecutable TUI fullscreen con tabs de Pipelines (lista filtrable + launch) e History (ejecuciones)"}


id: run_cmd_go_shell
name: run_cmd
kind: function
lang: go
domain: shell
version: 1.0.0
purity: impure
signature: func Run(name string, args ...string) core.Result[CmdResult]
description: Ejecuta un comando del sistema con timeout de 30 segundos y devuelve el resultado.
tags: ["shell","command","process","exec"]
uses_functions: []
uses_types: ["cmd_result_go_shell","result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: []
example:
  ```go
  result := Run("ls", "-la")
  ```

tests: []
file_path: functions/shell/run_cmd.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Implementacion en devfactory/shell.
content_hash: 03340d58d926e39caea6d195fadcf209093ccb2b0438e71a036afd5293f71b85
code:
  package shell

  import "fmt"

  // Implementation: github.com/lucasdataproyects/devfactory/shell

  // Run ejecuta un comando del sistema con timeout de 30 segundos y devuelve el resultado.
  func Run(name string, args ...string) (CmdResult, error) {
  	// stub — implementation in devfactory/shell
  	return CmdResult{}, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"name","desc":"nombre del comando (ej: ls, docker)"},{"name":"args","desc":"argumentos del comando (variadic)"}],"output":"Result[CmdResult] con stdout, stderr, exit code"}


id: run_cmd_timeout_go_shell
name: run_cmd_timeout
kind: function
lang: go
domain: shell
version: 1.0.0
purity: impure
signature: func RunWithTimeout(name string, timeout time.Duration, args ...string) core.Result[CmdResult]
description: Ejecuta un comando del sistema con timeout configurable.
tags: ["shell","command","process","exec","timeout"]
uses_functions: []
uses_types: ["cmd_result_go_shell","result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: ["time"]
example:
  ```go
  result := RunWithTimeout("curl", 5*time.Second, "https://example.com")
  ```

tests: []
file_path: functions/shell/run_cmd_timeout.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Implementacion en devfactory/shell.
content_hash: cbba369224f509041bb020894e57fe0c1055098baed819d148b2056f5dd49933
code:
  package shell

  import (
  	"fmt"
  	"time"
  )

  // Implementation: github.com/lucasdataproyects/devfactory/shell

  // RunWithTimeout ejecuta un comando del sistema con timeout configurable.
  func RunWithTimeout(name string, timeout time.Duration, args ...string) (CmdResult, error) {
  	// stub — implementation in devfactory/shell
  	return CmdResult{}, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"name","desc":"nombre del comando"},{"name":"timeout","desc":"duración máxima de ejecución"},{"name":"args","desc":"argumentos del comando (variadic)"}],"output":"Result[CmdResult] con stdout, stderr, exit code"}


id: run_pipe_go_shell
name: run_pipe
kind: function
lang: go
domain: shell
version: 1.0.0
purity: impure
signature: func RunPipe(commands ...string) core.Result[CmdResult]
description: Encadena multiples comandos con pipe y devuelve el resultado final.
tags: ["shell","command","process","pipe"]
uses_functions: []
uses_types: ["cmd_result_go_shell","result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: []
example:
  ```go
  result := RunPipe("ls -la", "grep go", "wc -l")
  ```

tests: []
file_path: functions/shell/run_pipe.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Implementacion en devfactory/shell.
content_hash: 1bd58988ccad1134eedae63eb5fe7983e4101f99bea29539cf6c6f624c17304e
code:
  package shell

  import "fmt"

  // Implementation: github.com/lucasdataproyects/devfactory/shell

  // RunPipe encadena multiples comandos con pipe y devuelve el resultado final.
  func RunPipe(commands ...string) (CmdResult, error) {
  	// stub — implementation in devfactory/shell
  	return CmdResult{}, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"commands","desc":"comandos a encadenar con pipe (variadic)"}],"output":"Result[CmdResult] con output combinado y exit code"}


id: run_shell_go_shell
name: run_shell
kind: function
lang: go
domain: shell
version: 1.0.0
purity: impure
signature: func RunShell(command string) core.Result[CmdResult]
description: Ejecuta un comando shell interpretado por /bin/sh.
tags: ["shell","command","process","exec"]
uses_functions: []
uses_types: ["cmd_result_go_shell","result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: []
example:
  ```go
  result := RunShell("ls -la | grep go")
  ```

tests: []
file_path: functions/shell/run_shell.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Implementacion en devfactory/shell.
content_hash: c8c189fac95f16cb8188717582523dd2be4b8438ff70d74de5ad2f0c43e7c3e6
code:
  package shell

  import "fmt"

  // Implementation: github.com/lucasdataproyects/devfactory/shell

  // RunShell ejecuta un comando shell interpretado por /bin/sh.
  func RunShell(command string) (CmdResult, error) {
  	// stub — implementation in devfactory/shell
  	return CmdResult{}, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"command","desc":"comando shell a ejecutar (ej: ls -la | grep go)"}],"output":"Result[CmdResult] con stdout, stderr, exit code"}


id: run_shell_timeout_go_shell
name: run_shell_timeout
kind: function
lang: go
domain: shell
version: 1.0.0
purity: impure
signature: func RunShellTimeout(command string, timeout time.Duration) core.Result[CmdResult]
description: Ejecuta un comando shell con timeout configurable.
tags: ["shell","command","process","exec","timeout"]
uses_functions: []
uses_types: ["cmd_result_go_shell","result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: ["time"]
example:
  ```go
  result := RunShellTimeout("long-running-cmd", 30*time.Second)
  ```

tests: []
file_path: functions/shell/run_shell_timeout.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Implementacion en devfactory/shell.
content_hash: 70df63fea1333e520a872aea87564bef8fb602062677794be8fdd4e68d34ee81
code:
  package shell

  import (
  	"fmt"
  	"time"
  )

  // Implementation: github.com/lucasdataproyects/devfactory/shell

  // RunShellTimeout ejecuta un comando shell con timeout configurable.
  func RunShellTimeout(command string, timeout time.Duration) (CmdResult, error) {
  	// stub — implementation in devfactory/shell
  	return CmdResult{}, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"command","desc":"comando shell a ejecutar"},{"name":"timeout","desc":"duración máxima de ejecución"}],"output":"Result[CmdResult] con stdout, stderr, exit code"}


id: which_go_shell
name: which
kind: function
lang: go
domain: shell
version: 1.0.0
purity: pure
signature: func Which(name string) core.Option[string]
description: Busca la ruta de un ejecutable en el PATH del sistema. Devuelve None si no existe.
tags: ["shell","path","lookup"]
uses_functions: []
uses_types: ["option_go_core"]
returns: ["option_go_core"]
imports: []
example:
  ```go
  path := Which("docker")
  ```

tests: []
file_path: functions/shell/which.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Implementacion en devfactory/shell.
content_hash: 54b406f02ed0cbadbc47f6a5ad64c4fce4399be7bcbf59bdc56eceb5fb162965
code:
  package shell

  // Implementation: github.com/lucasdataproyects/devfactory/shell

  // Which busca la ruta de un ejecutable en el PATH del sistema. Devuelve ("", false) si no existe.
  func Which(name string) (string, bool) {
  	// stub — implementation in devfactory/shell
  	return "", false
  }

params_schema:
  {"params":[{"name":"name","desc":"nombre del ejecutable a buscar"}],"output":"Option[string]: Some(ruta_absoluta) o None si no existe"}


id: clear_screen_go_tui
name: clear_screen
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func ClearScreen() string
description: Devuelve el codigo de escape ANSI para limpiar la pantalla del terminal.
tags: ["tui","ansi","terminal","screen"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: functions/tui/clear_screen.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7bec2db2888a90720d6fd92350ab025292de2588b6c76d269336d3a729fd40b4
params_schema: {"output":"string: código ANSI para limpiar pantalla"}
example:
  ```go
  fmt.Print(ClearScreen())
  // Limpia toda la pantalla y mueve el cursor a la posicion inicial
  ```

notes:
  Combina ESC[2J (borrar pantalla completa) con ESC[H (mover cursor a home). No tiene side effects: solo devuelve el string ANSI.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // ClearScreen devuelve el codigo de escape ANSI para limpiar la pantalla del terminal.
  func ClearScreen() string {
  	return "\033[2J\033[H"
  }


id: confirm_prompt_go_tui
name: confirm_prompt
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func Confirm(prompt string) core.Result[bool]
description: Muestra un dialogo de confirmacion Si/No en terminal y devuelve la eleccion del usuario.
tags: ["tui","confirm","prompt","interactive"]
uses_functions: []
uses_types: ["result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: []
tests: []
file_path: functions/tui/confirm_prompt.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d495d64f37038962ca2a7654f2d55cee4c42a0fdb2639b22561e191c73182859
example:
  ```go
  result := tui.Confirm("¿Deseas continuar?")
  if result.IsErr() {
      log.Fatal(result.Error())
  }
  if result.Unwrap() {
      fmt.Println("Confirmado")
  }
  ```

notes:
  Implementacion real en github.com/lucasdataproyects/devfactory/tui. Este stub existe para documentar la firma y mantener el registry.

code:
  package tui

  import "fmt"

  // Confirm muestra un prompt de confirmacion si/no en la terminal.
  func Confirm(prompt string) (bool, error) {
  	// stub — implementation in devfactory/tui
  	return false, fmt.Errorf("not implemented")
  }

params_schema:
  {"params":[{"name":"prompt","desc":"pregunta a mostrar al usuario"}],"output":"Result[bool]: true si confirmó, false si rechazó"}


id: dark_styles_go_tui
name: dark_styles
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func DarkStyles() Styles
tags: ["tui","styles","dark","constructor"]
uses_functions: []
uses_types: []
returns: ["styles_go_tui"]
imports: []
example:
  ```go
  styles := DarkStyles()
  model := NewProgressWithStyles(100.0, "Compilando", styles)
  ```

tests: []
file_path: functions/tui/dark_styles.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Equivalente a NewStyles(DarkTheme()). Usa esta funcion para aplicaciones TUI en terminales oscuras.
content_hash: ca82d3010ff3f40dc80878727ad3d86bf01fa0c8d015ef356e42f6fd13eef623
params_schema: {"output":"Styles: conjunto de estilos lipgloss oscuros"}
description:
  Construye estilos oscuros combinando DarkTheme con NewStyles. Atajo conveniente para terminales con fondo negro.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // DarkStyles crea estilos oscuros usando el tema dark.
  func DarkStyles() Styles {
  	// stub — implementation in devfactory/tui
  	return Styles{}
  }


id: dark_theme_go_tui
name: dark_theme
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func DarkTheme() Theme
tags: ["tui","theme","colors","dark","constructor"]
uses_functions: []
uses_types: []
returns: ["theme_go_tui"]
imports: []
example:
  ```go
  theme := DarkTheme()
  styles := NewStyles(theme)
  ```

tests: []
file_path: functions/tui/dark_theme.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Alternativa oscura al DefaultTheme. Colores de alto contraste para fondos negros.
content_hash: 41b83b51cb49a2ce8a1527c1bc259642774f56566a667f52f394cddc80c5c0d0
params_schema: {"output":"Theme: paleta de colores oscura"}
description:
  Construye un tema de colores oscuro para componentes TUI. Paleta optimizada para terminales con fondo negro.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // DarkTheme crea un tema de colores oscuro.
  func DarkTheme() Theme {
  	// stub — implementation in devfactory/tui
  	return Theme{}
  }


id: default_styles_go_tui
name: default_styles
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func DefaultStyles() Styles
tags: ["tui","styles","constructor"]
uses_functions: []
uses_types: []
returns: ["styles_go_tui"]
imports: []
example:
  ```go
  styles := DefaultStyles()
  model := NewSpinnerWithStyle("Cargando...", styles)
  ```

tests: []
file_path: functions/tui/default_styles.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Equivalente a NewStyles(DefaultTheme()). Usa esta funcion cuando no necesitas personalizar el tema.
content_hash: b11ea0f07200b67b574f431bfe016933ad498273724be658dda7adb9ddde3b2b
params_schema: {"output":"Styles: conjunto de estilos lipgloss por defecto"}
description:
  Construye estilos por defecto combinando DefaultTheme con NewStyles. Atajo conveniente para el caso comun.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // DefaultStyles crea estilos por defecto usando el tema base.
  func DefaultStyles() Styles {
  	// stub — implementation in devfactory/tui
  	return Styles{}
  }


id: default_theme_go_tui
name: default_theme
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func DefaultTheme() Theme
tags: ["tui","theme","colors","constructor"]
uses_functions: []
uses_types: []
returns: ["theme_go_tui"]
imports: []
example:
  ```go
  theme := DefaultTheme()
  styles := NewStyles(theme)
  ```

tests: []
file_path: functions/tui/default_theme.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Tema base del sistema de estilos. Usar DarkTheme para terminales con fondo oscuro.
content_hash: b0b3e6c5e61df5ea9974ae34e5458a027fb4170e77b619497a40ab2230212a6e
params_schema: {"output":"Theme: paleta de colores clara/por defecto"}
description:
  Construye el tema de colores por defecto para componentes TUI. Paleta clara optimizada para terminales con fondo blanco.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // DefaultTheme crea el tema de colores por defecto.
  func DefaultTheme() Theme {
  	// stub — implementation in devfactory/tui
  	return Theme{}
  }


id: hide_cursor_go_tui
name: hide_cursor
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func HideCursor() string
description: Devuelve el codigo de escape ANSI para ocultar el cursor del terminal.
tags: ["tui","ansi","terminal","cursor"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  fmt.Print(HideCursor())
  // Oculta el cursor del terminal usando DECTCEM
  ```

tests: []
file_path: functions/tui/hide_cursor.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 417ef4fa35e41278599091250fbe7e8bdec80f667cd229353ef7d0297c0729ae
params_schema: {"output":"string: código ANSI para ocultar el cursor"}
notes:
  Usa la secuencia DECTCEM (ESC[?25l) del estandar VT220. Recordar restaurar el cursor con ShowCursor al finalizar la aplicacion.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // HideCursor devuelve el codigo de escape ANSI para ocultar el cursor del terminal.
  func HideCursor() string {
  	return "\033[?25l"
  }


id: new_base_model_go_tui
name: new_base_model
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewBaseModel() BaseModel
tags: ["tui","base","constructor"]
uses_functions: []
uses_types: []
returns: ["base_model_go_tui"]
imports: []
example:
  ```go
  base := NewBaseModel()
  ```

tests: []
file_path: functions/tui/new_base_model.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: e2707cafbeaea45e60fd402191df039f97ffb6c17cb9e0bc10e5ab07c630b48d
params_schema: {"output":"BaseModel: modelo base con dimensiones y estilos"}
description:
  Construye un modelo base con dimensiones de terminal y estilos por defecto. Sirve como fundacion para componer modelos mas complejos.

notes:
  Modelo raiz que encapsula estado comun (dimensiones, estilos, keybindings) compartido por todos los componentes TUI.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewBaseModel crea un modelo base con configuracion por defecto.
  func NewBaseModel() BaseModel {
  	// stub — implementation in devfactory/tui
  	return BaseModel{}
  }


id: new_confirm_go_tui
name: new_confirm
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewConfirm(prompt string) ConfirmModel
description: Construye un modelo de dialogo de confirmacion con una pregunta si/no.
tags: ["tui","confirm","dialog","constructor"]
uses_functions: []
uses_types: []
returns: ["confirm_model_go_tui"]
imports: []
example:
  ```go
  model := NewConfirm("Deseas continuar?")
  ```

tests: []
file_path: functions/tui/new_confirm.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: bbdfb649d61ed0f2c6b69722d2ccd01bb1c689f1de930c1900167028e97cdc4e
notes:
  Dialogo binario si/no. El resultado se obtiene del modelo tras la interaccion del usuario en el loop de Bubble Tea.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewConfirm crea un modelo de dialogo de confirmacion si/no.
  func NewConfirm(prompt string) ConfirmModel {
  	// stub — implementation in devfactory/tui
  	return ConfirmModel{}
  }

params_schema:
  {"params":[{"name":"prompt","desc":"pregunta a mostrar en el diálogo"}],"output":"ConfirmModel: modelo de confirmación interactivo"}


id: new_filtered_list_go_tui
name: new_filtered_list
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewFilteredList(items []ListItem, placeholder string) FilteredListModel
tags: ["tui","list","filter","constructor"]
uses_functions: []
uses_types: ["list_item_go_tui"]
returns: ["filtered_list_model_go_tui"]
imports: []
tests: []
file_path: functions/tui/new_filtered_list.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Incluye un input de texto que filtra los items en tiempo real por coincidencia parcial.
content_hash: 06d931fd24b4712da8e2331b0f279d06b5c72a25c822e12c16bf4f812e4fe21f
description:
  Construye un modelo de lista con campo de busqueda integrado. El placeholder se muestra en el input de filtro cuando esta vacio.

example:
  ```go
  items := []ListItem{
      {Title: "main.go", Description: "Punto de entrada"},
      {Title: "server.go", Description: "Servidor HTTP"},
  }
  model := NewFilteredList(items, "Buscar archivo...")
  ```

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewFilteredList crea un modelo de lista con filtrado por texto.
  func NewFilteredList(items []ListItem, placeholder string) FilteredListModel {
  	// stub — implementation in devfactory/tui
  	return FilteredListModel{}
  }

params_schema:
  {"params":[{"name":"items","desc":"array de ListItem con título y descripción"},{"name":"placeholder","desc":"texto a mostrar en el input de filtro vacío"}],"output":"FilteredListModel: lista con búsqueda en tiempo real"}


id: new_list_go_tui
name: new_list
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewList(items []ListItem) ListModel
tags: ["tui","list","constructor"]
uses_functions: []
uses_types: ["list_item_go_tui"]
returns: ["list_model_go_tui"]
imports: []
tests: []
file_path: functions/tui/new_list.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 172cc7c4e96cdc34dc4041c45e6ea5e8893733d4a3c11e0173e495017800fd03
description:
  Construye un modelo de lista simple a partir de una coleccion de items. Cada item se renderiza como una fila seleccionable.

example:
  ```go
  items := []ListItem{
      {Title: "Opcion 1", Description: "Primera opcion"},
      {Title: "Opcion 2", Description: "Segunda opcion"},
  }
  model := NewList(items)
  ```

notes:
  Constructor puro para listas de seleccion simple. No permite seleccion multiple — usar NewMultiList para eso.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewList crea un modelo de lista simple a partir de items.
  func NewList(items []ListItem) ListModel {
  	// stub — implementation in devfactory/tui
  	return ListModel{}
  }

params_schema:
  {"params":[{"name":"items","desc":"array de ListItem para seleccionar"}],"output":"ListModel: lista simple seleccionable"}


id: new_multi_list_go_tui
name: new_multi_list
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewMultiList(items []ListItem) ListModel
tags: ["tui","list","multi","constructor"]
uses_functions: []
uses_types: ["list_item_go_tui"]
returns: ["list_model_go_tui"]
imports: []
tests: []
file_path: functions/tui/new_multi_list.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Variante de lista que habilita seleccion multiple con toggles por item.
content_hash: f8f97606e363475c1bf31e0d91573e7b90a594a3eb11a94e5c7a5797bf3bbe01
description:
  Construye un modelo de lista con seleccion multiple. Permite al usuario marcar varios items antes de confirmar.

example:
  ```go
  items := []ListItem{
      {Title: "Go", Description: "Lenguaje compilado"},
      {Title: "Python", Description: "Lenguaje interpretado"},
      {Title: "Rust", Description: "Lenguaje de sistemas"},
  }
  model := NewMultiList(items)
  ```

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewMultiList crea un modelo de lista con seleccion multiple.
  func NewMultiList(items []ListItem) ListModel {
  	// stub — implementation in devfactory/tui
  	return ListModel{}
  }

params_schema:
  {"params":[{"name":"items","desc":"array de ListItem para seleccionar múltiples"}],"output":"ListModel: lista con selección múltiple"}


id: new_multi_progress_go_tui
name: new_multi_progress
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewMultiProgress() MultiProgressModel
tags: ["tui","progress","multi","constructor"]
uses_functions: []
uses_types: []
returns: ["multi_progress_model_go_tui"]
imports: []
example:
  ```go
  model := NewMultiProgress()
  ```

tests: []
file_path: functions/tui/new_multi_progress.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 01b0fbee65c138ee46f9752e5b5969ec134d4c6bc407d47ce5bf813337773e2f
params_schema: {"output":"MultiProgressModel: contenedor para múltiples barras de progreso"}
description:
  Construye un modelo de progreso multiple vacio. Las barras individuales se agregan posteriormente via mensajes.

notes:
  Inicia sin barras. Cada barra se agrega dinamicamente via el loop de Bubble Tea, ideal para descargas paralelas o tareas concurrentes.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewMultiProgress crea un modelo de progreso multiple sin barras iniciales.
  func NewMultiProgress() MultiProgressModel {
  	// stub — implementation in devfactory/tui
  	return MultiProgressModel{}
  }


id: new_progress_go_tui
name: new_progress
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewProgress(total float64, label string) ProgressModel
description: Construye un modelo de barra de progreso con valor total y etiqueta descriptiva.
tags: ["tui","progress","constructor"]
uses_functions: []
uses_types: []
returns: ["progress_model_go_tui"]
imports: []
example:
  ```go
  model := NewProgress(100.0, "Descargando archivos")
  ```

tests: []
file_path: functions/tui/new_progress.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: El progreso inicia en 0. El avance se actualiza via mensajes en el loop de Bubble Tea.
content_hash: c98ec9a38d14fc41a68a44b308986b44022c76cbddd90b08e0e5c5929bdc48d3
code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewProgress crea un modelo de barra de progreso.
  func NewProgress(total float64, label string) ProgressModel {
  	// stub — implementation in devfactory/tui
  	return ProgressModel{}
  }

params_schema:
  {"params":[{"name":"total","desc":"valor total para la barra de progreso"},{"name":"label","desc":"etiqueta descriptiva de la operación"}],"output":"ProgressModel: barra de progreso"}


id: new_progress_with_styles_go_tui
name: new_progress_with_styles
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewProgressWithStyles(total float64, label string, styles Styles) ProgressModel
tags: ["tui","progress","styles","constructor"]
uses_functions: []
uses_types: ["styles_go_tui"]
returns: ["progress_model_go_tui"]
imports: []
example:
  ```go
  styles := DarkStyles()
  model := NewProgressWithStyles(50.0, "Indexando", styles)
  ```

tests: []
file_path: functions/tui/new_progress_with_styles.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Variante de NewProgress que permite personalizar la apariencia de la barra mediante Styles.
content_hash: 61d3a9ff3ef6c0ece4204e0b0b2dd4786772791f9a04d52bae09f7d58cc5b910
description:
  Construye un modelo de barra de progreso con valor total, etiqueta y estilos visuales personalizados.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewProgressWithStyles crea un modelo de barra de progreso con estilos personalizados.
  func NewProgressWithStyles(total float64, label string, styles Styles) ProgressModel {
  	// stub — implementation in devfactory/tui
  	return ProgressModel{}
  }

params_schema:
  {"params":[{"name":"total","desc":"valor total para la barra"},{"name":"label","desc":"etiqueta descriptiva"},{"name":"styles","desc":"Styles para aplicar a la barra"}],"output":"ProgressModel: barra de progreso estilizada"}


id: new_spinner_go_tui
name: new_spinner
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewSpinner(message string) SpinnerModel
description: Construye un modelo de spinner basico con un mensaje descriptivo. Usa el estilo por defecto.
tags: ["tui","spinner","constructor"]
uses_functions: []
uses_types: []
returns: ["spinner_model_go_tui"]
imports: []
example:
  ```go
  model := NewSpinner("Cargando datos...")
  ```

tests: []
file_path: functions/tui/new_spinner.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Spinner con estilos por defecto. Para personalizar apariencia, usar NewSpinnerWithStyle.
content_hash: 739365ccd5816559feb7745490f5f8d37e9085d1a4f226b6d93e2adcf6cf9901
code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewSpinner crea un modelo de spinner con un mensaje.
  func NewSpinner(message string) SpinnerModel {
  	// stub — implementation in devfactory/tui
  	return SpinnerModel{}
  }

params_schema:
  {"params":[{"name":"message","desc":"mensaje a mostrar junto al spinner"}],"output":"SpinnerModel: spinner con estilos por defecto"}


id: new_spinner_with_style_go_tui
name: new_spinner_with_style
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewSpinnerWithStyle(message string, styles Styles) SpinnerModel
description: Construye un modelo de spinner con mensaje y estilos visuales personalizados.
tags: ["tui","spinner","styles","constructor"]
uses_functions: []
uses_types: ["styles_go_tui"]
returns: ["spinner_model_go_tui"]
imports: []
tests: []
file_path: functions/tui/new_spinner_with_style.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Permite controlar colores y bordes del spinner mediante el sistema de estilos del registry.
content_hash: 7bfbfb42c48bf2e86442c40ee73cffde4138dc64dd24df93c97d9433cee809cc
example:
  ```go
  theme := DarkTheme()
  styles := NewStyles(theme)
  model := NewSpinnerWithStyle("Procesando...", styles)
  ```

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewSpinnerWithStyle crea un modelo de spinner con estilos personalizados.
  func NewSpinnerWithStyle(message string, styles Styles) SpinnerModel {
  	// stub — implementation in devfactory/tui
  	return SpinnerModel{}
  }

params_schema:
  {"params":[{"name":"message","desc":"mensaje a mostrar"},{"name":"styles","desc":"Styles para personalizar apariencia"}],"output":"SpinnerModel: spinner estilizado"}


id: new_spinner_with_timeout_go_tui
name: new_spinner_with_timeout
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewSpinnerWithTimeout(message string, timeout time.Duration) SpinnerWithTimeoutModel
tags: ["tui","spinner","timeout","constructor"]
uses_functions: []
uses_types: []
returns: ["spinner_with_timeout_model_go_tui"]
imports: ["time"]
example:
  ```go
  model := NewSpinnerWithTimeout("Esperando respuesta...", 30*time.Second)
  ```

tests: []
file_path: functions/tui/new_spinner_with_timeout.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 71f03e3dac9537b49c56464a99ccf23761bcd7fe5badef287c15ed3cade1652d
description:
  Construye un modelo de spinner con limite de tiempo. Si la operacion excede el timeout, el spinner se detiene automaticamente.

notes:
  El timeout es configuracion pura — el side effect de cancelar ocurre en el runtime de Bubble Tea, no en el constructor.

code:
  package tui

  import "time"

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewSpinnerWithTimeout crea un modelo de spinner que se cancela tras un timeout.
  func NewSpinnerWithTimeout(message string, timeout time.Duration) SpinnerWithTimeoutModel {
  	// stub — implementation in devfactory/tui
  	return SpinnerWithTimeoutModel{}
  }

params_schema:
  {"params":[{"name":"message","desc":"mensaje del spinner"},{"name":"timeout","desc":"duración máxima antes de auto-parada"}],"output":"SpinnerWithTimeoutModel: spinner con límite de tiempo"}


id: new_styles_go_tui
name: new_styles
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func NewStyles(theme Theme) Styles
tags: ["tui","styles","constructor"]
uses_functions: []
uses_types: ["theme_go_tui"]
returns: ["styles_go_tui"]
imports: []
example:
  ```go
  theme := DefaultTheme()
  styles := NewStyles(theme)
  ```

tests: []
file_path: functions/tui/new_styles.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 126a4c74daa3cde84ea29e494ff68620bf8f5e8e8e1d29e22fc29a50a00087a0
description:
  Construye un conjunto de estilos lipgloss a partir de un tema de colores. Los estilos resultantes se aplican a todos los componentes TUI.

notes:
  Convierte un Theme (paleta de colores) en Styles (estilos lipgloss aplicables). Es el puente entre la configuracion de colores y el renderizado.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // NewStyles crea estilos a partir de un tema.
  func NewStyles(theme Theme) Styles {
  	// stub — implementation in devfactory/tui
  	return Styles{}
  }

params_schema:
  {"params":[{"name":"theme","desc":"Theme con paleta de colores"}],"output":"Styles: estilos lipgloss aplicables a componentes"}


id: print_error_go_tui
name: print_error
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func PrintError(msg string)
description: Imprime un mensaje con estilo de error (rojo) en stderr.
tags: ["tui","print","error","output"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```go
  tui.PrintError("No se pudo conectar al servidor")
  ```

tests: []
file_path: functions/tui/print_error.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: cf2ee342f91339636be0f7579373b7935f8a63c32f7e5baffd74f5ccffcb96a7
notes:
  Implementacion real en github.com/lucasdataproyects/devfactory/tui. Este stub existe para documentar la firma y mantener el registry.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // PrintError imprime un mensaje con estilo de error (rojo) en stderr.
  func PrintError(msg string) {
  	// stub — implementation in devfactory/tui
  }

params_schema:
  {"params":[{"name":"msg","desc":"mensaje de error a imprimir"}],"output":"sin salida; imprime a stderr con estilo rojo"}


id: print_info_go_tui
name: print_info
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func PrintInfo(msg string)
description: Imprime un mensaje con estilo informativo (cyan) en stdout.
tags: ["tui","print","info","output"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```go
  tui.PrintInfo("Procesando 42 archivos...")
  ```

tests: []
file_path: functions/tui/print_info.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 69a49be5462410a309915fd575809952916888fdd4ec955bae296ec9d2c0a049
notes:
  Implementacion real en github.com/lucasdataproyects/devfactory/tui. Este stub existe para documentar la firma y mantener el registry.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // PrintInfo imprime un mensaje con estilo informativo (cyan) en stdout.
  func PrintInfo(msg string) {
  	// stub — implementation in devfactory/tui
  }

params_schema:
  {"params":[{"name":"msg","desc":"mensaje informativo a imprimir"}],"output":"sin salida; imprime a stdout con estilo cyan"}


id: print_muted_go_tui
name: print_muted
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func PrintMuted(msg string)
description: Imprime un mensaje con estilo atenuado (gris) en stdout.
tags: ["tui","print","muted","output"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```go
  tui.PrintMuted("Ultima actualizacion: hace 5 minutos")
  ```

tests: []
file_path: functions/tui/print_muted.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5c829a8809f44c668812e9a77f362f0b55975bbcfe94a2bba7160244e7b8308d
notes:
[cell repeats an earlier one in this file: 133 characters]

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // PrintMuted imprime un mensaje con estilo atenuado (gris) en stdout.
  func PrintMuted(msg string) {
  	// stub — implementation in devfactory/tui
  }

params_schema:
  {"params":[{"name":"msg","desc":"mensaje atenuado a imprimir"}],"output":"sin salida; imprime a stdout con estilo gris"}


id: print_success_go_tui
name: print_success
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func PrintSuccess(msg string)
description: Imprime un mensaje con estilo de exito (verde) en stdout.
tags: ["tui","print","success","output"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```go
  tui.PrintSuccess("Operacion completada correctamente")
  ```

tests: []
file_path: functions/tui/print_success.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: dbf59cafabb4fdbf0185841363c5b339e78d426b82fa7c04e97200692a1a4e96
notes:
[cell repeats an earlier one in this file: 133 characters]

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // PrintSuccess imprime un mensaje con estilo de exito (verde) en stdout.
  func PrintSuccess(msg string) {
  	// stub — implementation in devfactory/tui
  }

params_schema:
  {"params":[{"name":"msg","desc":"mensaje de éxito a imprimir"}],"output":"sin salida; imprime a stdout con estilo verde"}


id: print_warning_go_tui
name: print_warning
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func PrintWarning(msg string)
description: Imprime un mensaje con estilo de advertencia (naranja) en stdout.
tags: ["tui","print","warning","output"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```go
  tui.PrintWarning("El archivo sera sobreescrito")
  ```

tests: []
file_path: functions/tui/print_warning.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b6c197a5dc678ec5aa3c325c6f5f10b6f006ebd8120a2975cc532c008266779d
notes:
[cell repeats an earlier one in this file: 133 characters]

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // PrintWarning imprime un mensaje con estilo de advertencia (naranja) en stdout.
  func PrintWarning(msg string) {
  	// stub — implementation in devfactory/tui
  }

params_schema:
  {"params":[{"name":"msg","desc":"mensaje de advertencia a imprimir"}],"output":"sin salida; imprime a stdout con estilo naranja"}


id: quit_msg_go_tui
name: quit_msg
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func Quit() tea.Msg
description: Devuelve un mensaje de salida para el bucle de Bubble Tea.
tags: ["tui","quit","message","bubbletea"]
uses_functions: []
uses_types: []
returns: []
imports: ["github.com/charmbracelet/bubbletea"]
example:
  ```go
  msg := Quit()
  // msg es tea.QuitMsg{}, usado para terminar el event loop de Bubble Tea
  ```

tests: []
file_path: functions/tui/quit_msg.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 0a615073f453fd2b776ce8ce4a7c9fff380830c668e743c63f085d75a8407631
params_schema: {"output":"tea.Msg: mensaje de salida para el event loop"}
notes:
  Wrapper puro sobre tea.QuitMsg{}. Util para componer en pipelines o pasar como callback sin exponer el tipo directamente.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  import tea "github.com/charmbracelet/bubbletea"

  // Quit devuelve un mensaje de salida para el bucle de Bubble Tea.
  func Quit() tea.Msg {
  	return tea.QuitMsg{}
  }


id: run_fullscreen_go_tui
name: run_fullscreen
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func RunFullscreen[T tea.Model](model T) core.Result[T]
description: Ejecuta un modelo Bubble Tea en modo fullscreen.
tags: ["tui","runner","fullscreen","bubbletea","generic"]
uses_functions: []
uses_types: ["result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: ["github.com/charmbracelet/bubbletea"]
tests: []
file_path: functions/tui/run_fullscreen.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 55752626346e172a73b663cfc4b8ecf19af0d2c457fc7282808a73788ce438ea
example:
  ```go
  result := tui.RunFullscreen(myModel)
  if result.IsErr() {
      log.Fatal(result.Error())
  }
  finalModel := result.Unwrap()
  ```

notes:
[cell repeats an earlier one in this file: 133 characters]

code:
  package tui

  import tea "github.com/charmbracelet/bubbletea"

  // RunFullscreen ejecuta un modelo Bubbletea en modo fullscreen.
  func RunFullscreen[T tea.Model](model T) (T, error) {
  	// stub — implementation in devfactory/tui
  	var zero T
  	return zero, nil
  }

params_schema:
  {"params":[{"name":"model","desc":"modelo Bubble Tea a ejecutar"}],"output":"Result[T]: modelo final o error"}


id: run_model_go_tui
name: run_model
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func RunModel[T tea.Model](model T) core.Result[T]
description: Ejecuta un modelo Bubble Tea y devuelve el modelo final o error.
tags: ["tui","runner","bubbletea","generic"]
uses_functions: []
uses_types: ["result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: ["github.com/charmbracelet/bubbletea"]
tests: []
file_path: functions/tui/run_model.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 8c5513003396d78c3c69865a05546414a2e4cb1086e3d2d6baf635b51c8e1e2c
example:
  ```go
  result := tui.RunModel(myModel)
  if result.IsErr() {
      log.Fatal(result.Error())
  }
  finalModel := result.Unwrap()
  ```

notes:
[cell repeats an earlier one in this file: 133 characters]

code:
  package tui

  import tea "github.com/charmbracelet/bubbletea"

  // RunModel ejecuta un modelo Bubbletea y devuelve el modelo final.
  func RunModel[T tea.Model](model T) (T, error) {
  	// stub — implementation in devfactory/tui
  	var zero T
  	return zero, nil
  }

params_schema:
  {"params":[{"name":"model","desc":"modelo Bubble Tea a ejecutar"}],"output":"Result[T]: modelo final o error"}


id: run_with_mouse_go_tui
name: run_with_mouse
kind: function
lang: go
domain: tui
version: 1.0.0
purity: impure
signature: func RunWithMouseSupport[T tea.Model](model T) core.Result[T]
description: Ejecuta un modelo Bubble Tea con soporte de raton habilitado.
tags: ["tui","runner","mouse","bubbletea","generic"]
uses_functions: []
uses_types: ["result_go_core"]
returns: ["result_go_core"]
error_type: error_go_core
imports: ["github.com/charmbracelet/bubbletea"]
tests: []
file_path: functions/tui/run_with_mouse.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 415a6b1ff0d325d05c5706674ee74f093642cf5e096875606002a85b93c0dda3
example:
  ```go
  result := tui.RunWithMouseSupport(myModel)
  if result.IsErr() {
      log.Fatal(result.Error())
  }
  finalModel := result.Unwrap()
  ```

notes:
[cell repeats an earlier one in this file: 133 characters]

code:
  package tui

  import tea "github.com/charmbracelet/bubbletea"

  // RunWithMouseSupport ejecuta un modelo Bubbletea con soporte de mouse.
  func RunWithMouseSupport[T tea.Model](model T) (T, error) {
  	// stub — implementation in devfactory/tui
  	var zero T
  	return zero, nil
  }

params_schema:
  {"params":[{"name":"model","desc":"modelo Bubble Tea a ejecutar con soporte de ratón"}],"output":"Result[T]: modelo final o error"}


id: show_cursor_go_tui
name: show_cursor
kind: function
lang: go
domain: tui
version: 1.0.0
purity: pure
signature: func ShowCursor() string
description: Devuelve el codigo de escape ANSI para mostrar el cursor del terminal.
tags: ["tui","ansi","terminal","cursor"]
uses_functions: []
uses_types: []
returns: []
imports: []
example:
  ```go
  fmt.Print(ShowCursor())
  // Restaura la visibilidad del cursor del terminal
  ```

tests: []
file_path: functions/tui/show_cursor.go
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 30bc8ea3a756a2b14b276efca20b8988826002c489745685cddba3504417c953
params_schema: {"output":"string: código ANSI para mostrar el cursor"}
notes:
  Usa la secuencia DECTCEM (ESC[?25h) del estandar VT220. Complemento de HideCursor. Llamar siempre al finalizar la aplicacion para restaurar el estado del terminal.

code:
  package tui

  // Implementation: github.com/lucasdataproyects/devfactory/tui

  // ShowCursor devuelve el codigo de escape ANSI para mostrar el cursor del terminal.
  func ShowCursor() string {
  	return "\033[?25h"
  }


id: assert_docker_container_running_bash_infra
name: assert_docker_container_running
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: assert_docker_container_running(container_name: string) -> void
tags: ["assert","docker","container","running","validation","infra","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/assert_docker_container_running.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 145d610fbb3cbe56b2beee02a4de3ff4d79a866082f84bf94d03c6b571681d97
description:
  Verifica que un contenedor Docker está corriendo. Sale con exit code 1 si no está activo, con mensaje a stderr.

example:
  ```bash
  source functions/infra/assert_docker_container_running.sh

  assert_docker_container_running metabase
  echo "Contenedor activo, continuando..."
  ```

notes:
  Usa `docker ps --format '{{.Names}}'` con grep anclado (`^name$`) para evitar matches parciales (ej: "metabase" no matchea "metabase-test").

  Output limpio: void en éxito. El mensaje de error en stderr no incluye lista de contenedores activos — eso es responsabilidad del pipeline/caller.

  Requiere que `docker` esté en PATH. Combinar con `assert_command_exists` antes de llamar.

code:
  # assert_docker_container_running
  # --------------------------------
  # Verifica que un contenedor Docker está corriendo.
  # No produce output a stdout en caso de éxito.
  # Sale con exit code 1 si el contenedor no está corriendo,
  # con mensaje descriptivo a stderr.
  #
  # USO (sourced):
  #   source assert_docker_container_running.sh
  #   assert_docker_container_running metabase

  assert_docker_container_running() {
      local container_name="$1"

      if ! docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then
          echo "assert_docker_container_running: el contenedor '$container_name' no está corriendo" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"container_name","desc":"nombre del contenedor Docker a verificar"}],"output":"sin salida; exit code 0 si existe y está corriendo, 1 si no"}


id: docker_cp_file_bash_infra
name: docker_cp_file
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: docker_cp_file(local_path: string, container_name: string, dest_path: string) -> string
tags: ["docker","cp","copy","file","container","transfer","infra","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/docker_cp_file.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 34cf8d182c9154609e1f09f14513194ccc85fc91b55b155bf06f0bd6ffe90528
description:
  Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide. Imprime JSON con local_size y remote_size a stdout. Sale con exit code 1 si docker cp falla o los tamaños difieren.

example:
  ```bash
  source functions/infra/docker_cp_file.sh

  result=$(docker_cp_file /home/lucas/fn_registry/registry.db metabase /registry.db)
  echo "$result"
  # {"local_size":524288,"remote_size":524288}

  local_size=$(echo "$result" | grep -o '"local_size":[0-9]*' | cut -d: -f2)
  ```

notes:
  La verificación de tamaño usa `docker exec stat -c%s` sobre el contenedor destino. Si `stat` no está disponible en el contenedor, `remote_size` será -1 y la función fallará.

  Output a stdout: JSON minificado con campos `local_size` y `remote_size` (enteros, bytes).

  Usa `printf` en lugar de `echo` para garantizar que no haya newline extra en el JSON.

code:
  # docker_cp_file
  # --------------
  # Copia un archivo local a un contenedor Docker y verifica que el tamaño coincide.
  # Imprime JSON con local_size y remote_size a stdout si la copia es exitosa.
  # Sale con exit code 1 si docker cp falla o si los tamaños no coinciden.
  #
  # USO (sourced):
  #   source docker_cp_file.sh
  #   result=$(docker_cp_file /ruta/local.db metabase /dest/path.db)

  docker_cp_file() {
      local local_path="$1"
      local container_name="$2"
      local dest_path="$3"

      if ! docker cp "$local_path" "${container_name}:${dest_path}" 2>/dev/null; then
          echo "docker_cp_file: fallo al copiar '$local_path' a '${container_name}:${dest_path}'" >&2
          return 1
      fi

      local local_size
      local_size=$(stat -c%s "$local_path")

      local remote_size
      remote_size=$(docker exec "$container_name" stat -c%s "$dest_path" 2>/dev/null || echo "-1")

      if [ "$local_size" != "$remote_size" ]; then
          echo "docker_cp_file: tamaños no coinciden (local=${local_size}, remoto=${remote_size})" >&2
          return 1
      fi

      printf '{"local_size":%s,"remote_size":%s}' "$local_size" "$remote_size"
  }

params_schema:
  {"params":[{"name":"local_path","desc":"ruta del archivo local a copiar"},{"name":"container_name","desc":"nombre del contenedor Docker destino"},{"name":"dest_path","desc":"ruta destino dentro del contenedor"}],"output":"JSON con local_size y remote_size en bytes"}


id: init_uv_venv_bash_infra
name: init_uv_venv
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: init_uv_venv([project_dir: string]) -> string
tags: ["python","venv","uv","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/init_uv_venv.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f0316721482ff0715ed3b38820d44506b9124ab7dbecc48b878802fe579c3dad
description:
  Crea un virtualenv Python con uv en el directorio dado si no existe. Fallback a python3 -m venv. Retorna la ruta del venv.

example:
  ```bash
  source init_uv_venv.sh
  venv=$(init_uv_venv /home/lucas/analysis/finanzas)
  echo "Venv creado en: $venv"

  # Idempotente — si ya existe, retorna la ruta sin recrear
  venv=$(init_uv_venv .)
  ```

notes:
  Idempotente: si el venv ya existe con un python valido, retorna la ruta sin hacer nada. Prefiere uv por velocidad, usa python3 como fallback.

code:
  # init_uv_venv
  # -------------
  # Crea un venv con uv en el directorio especificado si no existe.
  # Fallback a python -m venv si uv no esta disponible.
  # Imprime la ruta del venv a stdout.
  #
  # USO (sourced):
  #   source init_uv_venv.sh
  #   venv_path=$(init_uv_venv /path/to/project)

  init_uv_venv() {
      local project_dir="${1:-.}"
      local venv_path="${project_dir}/.venv"

      if [ -d "$venv_path" ] && [ -f "$venv_path/bin/python" ]; then
          echo "$venv_path"
          return 0
      fi

      if command -v uv &>/dev/null; then
          (cd "$project_dir" && uv venv) >/dev/null 2>&1
      elif command -v python3 &>/dev/null; then
          python3 -m venv "$venv_path"
      else
          echo "init_uv_venv: ni uv ni python3 disponibles" >&2
          return 1
      fi

      if [ ! -f "$venv_path/bin/python" ]; then
          echo "init_uv_venv: fallo al crear venv en $venv_path" >&2
          return 1
      fi

      echo "$venv_path"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto donde crear el venv (default: directorio actual)"}],"output":"ruta absoluta del venv creado o existente"}


id: install_nbconvert_bash_infra
name: install_nbconvert
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_nbconvert(project_dir: string) -> void
tags: ["jupyter","nbconvert","pdf","export","playwright","python","uv"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source install_nbconvert.sh
  install_nbconvert /home/lucas/analysis/finanzas
  ```

tests: []
file_path: bash/functions/infra/install_nbconvert.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: be6ee004a968c0023937c202d2e84019de4b1d71368e03571d611deed2063d3b
description:
  Instala nbconvert y playwright con chromium en un proyecto uv existente. Idempotente: uv add no reinstala si los paquetes ya estan presentes.

notes:
  Requiere que el venv ya exista (usa `init_uv_venv` antes). La instalacion de chromium via `uv run playwright install chromium` puede tardar la primera vez. La salida de playwright se suprime si tiene exito — solo se muestra si hay un error.

code:
  # install_nbconvert
  # ------------------
  # Instala nbconvert y playwright con chromium en un proyecto uv existente.
  # Idempotente: uv add no reinstala si los paquetes ya estan presentes.
  #
  # USO (sourced):
  #   source install_nbconvert.sh
  #   install_nbconvert /path/to/project

  install_nbconvert() {
      local project_dir="$1"

      if [ -z "$project_dir" ]; then
          echo "install_nbconvert: se requiere project_dir" >&2
          return 1
      fi

      if [ ! -d "$project_dir/.venv" ]; then
          echo "install_nbconvert: no existe .venv en $project_dir — ejecuta init_uv_venv primero" >&2
          return 1
      fi

      # Instalar nbconvert y playwright via uv add
      (cd "$project_dir" && uv add nbconvert playwright 2>&1)

      # Instalar chromium — capturar output, solo mostrar si hay error
      local playwright_output
      if ! playwright_output=$(cd "$project_dir" && uv run playwright install chromium 2>&1); then
          echo "$playwright_output" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto con venv existente"}],"output":"sin salida"}


id: install_nordvpn_bash_infra
name: install_nordvpn
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: install_nordvpn() -> void
tags: ["vpn","nordvpn","install","infra","wsl2"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/install_nordvpn.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 57b7aebbc251da1f0d0c4680aef0f2734275a1932a0bfb85c596f41f481f047d
params_schema: {"output":"sin salida; muestra estado de instalación"}
description:
  Instala NordVPN CLI en Ubuntu/Debian (incluido WSL2). Configura repositorio oficial, instala paquete y habilita servicio nordvpnd. Idempotente.

example:
  ```bash
  source install_nordvpn.sh
  install_nordvpn
  # nordvpn ya instalado: NordVPN Version 3.x.x
  # — o —
  # Instalando NordVPN CLI...
  # NordVPN instalado: NordVPN Version 3.x.x
  # NOTA: ejecuta 'nordvpn login' para autenticarte
  ```

notes:
  Usa el script de instalacion oficial de NordVPN. En WSL2 sin systemd, levanta nordvpnd manualmente. Agrega el usuario al grupo nordvpn para evitar sudo en comandos posteriores. Despues de instalar, se requiere `nordvpn login` para autenticarse.

code:
  # install_nordvpn
  # ---------------
  # Instala NordVPN CLI en Ubuntu/Debian (incluido WSL2).
  # Configura el repositorio oficial, instala el paquete y habilita el servicio.
  # Si ya esta instalado, no hace nada.
  #
  # USO (sourced):
  #   source install_nordvpn.sh
  #   install_nordvpn

  install_nordvpn() {
      if command -v nordvpn &>/dev/null; then
          echo "nordvpn ya instalado: $(nordvpn version 2>/dev/null)"
          return 0
      fi

      echo "Instalando NordVPN CLI..."

      # Descargar e instalar via script oficial
      sh <(curl -sSf https://downloads.nordcdn.com/apps/linux/install.sh) 2>&1

      if ! command -v nordvpn &>/dev/null; then
          echo "install_nordvpn: fallo la instalacion" >&2
          return 1
      fi

      # Agregar usuario al grupo nordvpn para evitar sudo
      sudo usermod -aG nordvpn "$USER" 2>/dev/null || true

      # Habilitar servicio (systemd o manual para WSL2)
      if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
          sudo systemctl enable --now nordvpnd 2>/dev/null || true
      else
          # WSL2 sin systemd — levantar daemon manualmente
          sudo nordvpnd &>/dev/null &
          sleep 2
      fi

      echo "NordVPN instalado: $(nordvpn version 2>/dev/null)"
      echo "NOTA: ejecuta 'nordvpn login' para autenticarte"
  }


id: nordvpn_connect_bash_infra
name: nordvpn_connect
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_connect(country?: string, city?: string) -> json
tags: ["vpn","nordvpn","connect","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_connect.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ccdb98fc2ce2c69f41b1dcaaaac86d601b147ace796f93091c8bba9d47dd93e4
description:
  Conecta a NordVPN por pais, ciudad o servidor especifico. Sin argumentos conecta al mejor servidor disponible. Devuelve JSON con resultado.

example:
  ```bash
  source nordvpn_connect.sh

  nordvpn_connect
  # {"ok":true,"server":"us1234.nordvpn.com","country":"auto","city":"auto"}

  nordvpn_connect Spain
  # {"ok":true,"server":"es42.nordvpn.com","country":"Spain","city":"auto"}

  nordvpn_connect Spain Madrid
  # {"ok":true,"server":"es15.nordvpn.com","country":"Spain","city":"Madrid"}
  ```

notes:
  Requiere NordVPN CLI instalado y autenticado (`nordvpn login`). La salida JSON facilita composicion con otros scripts y pipelines. Si ya hay una conexion activa, NordVPN reconecta automaticamente al nuevo destino.

code:
  # nordvpn_connect
  # ---------------
  # Conecta a NordVPN. Acepta pais, ciudad o servidor especifico.
  # Sin argumentos conecta al mejor servidor disponible.
  # Imprime JSON con el resultado de la conexion.
  #
  # USO (sourced):
  #   source nordvpn_connect.sh
  #   nordvpn_connect                    # mejor servidor
  #   nordvpn_connect Spain              # por pais
  #   nordvpn_connect Spain Madrid       # por ciudad
  #   nordvpn_connect Spain '#42'        # servidor especifico

  nordvpn_connect() {
      local country="${1:-}"
      local city="${2:-}"

      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local args=()
      [ -n "$country" ] && args+=("$country")
      [ -n "$city" ] && args+=("$city")

      local output
      output=$(nordvpn connect "${args[@]}" 2>&1)
      local rc=$?

      if [ $rc -eq 0 ] && echo "$output" | grep -qi "connected"; then
          local server
          server=$(echo "$output" | grep -oP '(?<=to )\S+' | head -1)
          echo "{\"ok\":true,\"server\":\"${server}\",\"country\":\"${country:-auto}\",\"city\":\"${city:-auto}\"}"
      else
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"country","desc":"país de destino (opcional; default: auto)"},{"name":"city","desc":"ciudad de destino (opcional; default: auto)"}],"output":"JSON con ok, server, country, city"}


id: nordvpn_disconnect_bash_infra
name: nordvpn_disconnect
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_disconnect() -> json
tags: ["vpn","nordvpn","disconnect","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source nordvpn_disconnect.sh

  nordvpn_disconnect
  # {"ok":true,"status":"disconnected"}
  ```

tests: []
file_path: bash/functions/infra/nordvpn_disconnect.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Idempotente: si no hay conexion activa, retorna ok sin error. Requiere NordVPN CLI instalado.
content_hash: ba29b2977ffedf5c85c1264d693348eb21aeefa38636ff85c815afb7e5acafc8
params_schema: {"output":"JSON con ok y status"}
description:
  Desconecta de NordVPN. Idempotente — si no hay conexion activa retorna ok. Devuelve JSON con resultado.

code:
  # nordvpn_disconnect
  # ------------------
  # Desconecta de NordVPN. Idempotente — si no hay conexion activa, retorna ok.
  # Imprime JSON con el resultado.
  #
  # USO (sourced):
  #   source nordvpn_disconnect.sh
  #   nordvpn_disconnect

  nordvpn_disconnect() {
      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local output
      output=$(nordvpn disconnect 2>&1)
      local rc=$?

      if [ $rc -eq 0 ] || echo "$output" | grep -qi "not connected\|disconnected"; then
          echo '{"ok":true,"status":"disconnected"}'
      else
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi
  }


id: nordvpn_get_ip_bash_infra
name: nordvpn_get_ip
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_get_ip() -> json
tags: ["vpn","nordvpn","ip","infra","network","verification"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_get_ip.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f056919ea70f5d64d7dee9f505ba5657dd47102db33419cb12c8b1c8d9bec4ea
params_schema: {"output":"JSON con ok, ip, vpn_connected, vpn_server, source"}
description:
  Obtiene IP publica actual con fallback entre multiples servicios. Indica si la conexion VPN esta activa y el servidor usado.

example:
  ```bash
  source nordvpn_get_ip.sh

  # Con VPN activa:
  nordvpn_get_ip
  # {"ok":true,"ip":"185.x.x.x","vpn_connected":true,"vpn_server":"es42.nordvpn.com","source":"https://api.ipify.org"}

  # Sin VPN:
  nordvpn_get_ip
  # {"ok":true,"ip":"88.x.x.x","vpn_connected":false,"vpn_server":"","source":"https://api.ipify.org"}
  ```

notes:
  Usa ipify.org como servicio primario con fallback a ifconfig.me e icanhazip.com. Timeout de 5 segundos por servicio. Util para verificar que el tunel VPN esta activo antes de ejecutar operaciones sensibles a la IP.

code:
  # nordvpn_get_ip
  # --------------
  # Obtiene la IP publica actual para verificar que el tunel VPN funciona.
  # Usa multiples servicios como fallback.
  #
  # USO (sourced):
  #   source nordvpn_get_ip.sh
  #   nordvpn_get_ip

  nordvpn_get_ip() {
      local ip=""
      local source=""

      # Intentar multiples servicios
      for svc in "https://api.ipify.org" "https://ifconfig.me" "https://icanhazip.com"; do
          ip=$(curl -s --max-time 5 "$svc" 2>/dev/null)
          if echo "$ip" | grep -qP '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'; then
              source="$svc"
              break
          fi
          ip=""
      done

      if [ -z "$ip" ]; then
          echo '{"ok":false,"error":"no se pudo obtener IP publica"}' >&2
          return 1
      fi

      # Si nordvpn esta disponible, incluir info de conexion
      local connected="false"
      local vpn_server=""
      if command -v nordvpn &>/dev/null; then
          local status_output
          status_output=$(nordvpn status 2>/dev/null)
          if echo "$status_output" | grep -qi "connected"; then
              connected="true"
              vpn_server=$(echo "$status_output" | grep -iP "hostname|server" | head -1 | sed 's/.*: *//')
          fi
      fi

      echo "{\"ok\":true,\"ip\":\"$ip\",\"vpn_connected\":$connected,\"vpn_server\":\"$vpn_server\",\"source\":\"$source\"}"
  }


id: nordvpn_list_cities_bash_infra
name: nordvpn_list_cities
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_list_cities(country: string) -> json
description: Lista ciudades disponibles de un pais en NordVPN como array JSON ordenado.
tags: ["vpn","nordvpn","cities","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_list_cities.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ade8b580824fc744cac0e9636f1614e24f323d9d41390564f47e53a0538bbf9a
example:
  ```bash
  source nordvpn_list_cities.sh

  nordvpn_list_cities Spain
  # {"ok":true,"country":"Spain","count":2,"cities":["Barcelona","Madrid"]}

  nordvpn_list_cities "United_States"
  # {"ok":true,"country":"United_States","count":15,"cities":["Atlanta","Buffalo",...]}
  ```

notes:
  El nombre de pais debe coincidir con lo que devuelve `nordvpn countries`. Usa underscores para paises compuestos (ej: United_States). Las ciudades se devuelven con espacios.

code:
  # nordvpn_list_cities
  # -------------------
  # Lista las ciudades disponibles de un pais en NordVPN como array JSON.
  #
  # USO (sourced):
  #   source nordvpn_list_cities.sh
  #   nordvpn_list_cities Spain

  nordvpn_list_cities() {
      local country="${1:?nordvpn_list_cities: se requiere pais como argumento}"

      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local output
      output=$(nordvpn cities "$country" 2>&1)
      local rc=$?

      if [ $rc -ne 0 ]; then
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi

      echo "$output" | python3 -c '
  import sys, json, re

  country = "'"$country"'"
  text = sys.stdin.read()
  text = re.sub(r"\x1b\[[0-9;]*m", "", text)
  text = re.sub(r"[\t\r]", " ", text)
  cities = [c.strip().replace("_", " ") for c in re.split(r"[,\n]+", text) if c.strip() and c.strip() != "-"]
  cities = [c for c in cities if len(c) > 1]
  cities.sort()
  print(json.dumps({"ok": True, "country": country, "count": len(cities), "cities": cities}))
  '
  }

params_schema:
  {"params":[{"name":"country","desc":"nombre del país en NordVPN (ej: Spain, United_States)"}],"output":"JSON con ok, country, count, cities (array de strings)"}


id: nordvpn_list_countries_bash_infra
name: nordvpn_list_countries
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_list_countries() -> json
description: Lista paises disponibles en NordVPN como array JSON ordenado alfabeticamente.
tags: ["vpn","nordvpn","countries","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_list_countries.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b8e9dae18e9fc5505238584cdeb997cdebe47eb15e130254bc667c1f035525df
params_schema: {"output":"JSON con ok, count, countries (array de strings ordenado alfabéticamente)"}
example:
  ```bash
  source nordvpn_list_countries.sh

  nordvpn_list_countries
  # {"ok":true,"count":60,"countries":["Albania","Argentina","Australia",...,"United States","Vietnam"]}
  ```

notes:
  Parsea la salida de `nordvpn countries` eliminando codigos ANSI y normalizando separadores. Los nombres de paises se devuelven con espacios en vez de underscores.

code:
  # nordvpn_list_countries
  # ----------------------
  # Lista los paises disponibles en NordVPN como array JSON.
  #
  # USO (sourced):
  #   source nordvpn_list_countries.sh
  #   nordvpn_list_countries

  nordvpn_list_countries() {
      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local output
      output=$(nordvpn countries 2>&1)
      local rc=$?

      if [ $rc -ne 0 ]; then
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi

      echo "$output" | python3 -c '
  import sys, json, re

  text = sys.stdin.read()
  text = re.sub(r"\x1b\[[0-9;]*m", "", text)
  text = re.sub(r"[\t\r]", " ", text)
  # Split by comma, whitespace, or newline and clean
  countries = [c.strip().replace("_", " ") for c in re.split(r"[,\n]+", text) if c.strip() and c.strip() != "-"]
  countries = [c for c in countries if len(c) > 1]
  countries.sort()
  print(json.dumps({"ok": True, "count": len(countries), "countries": countries}))
  '
  }


id: nordvpn_set_protocol_bash_infra
name: nordvpn_set_protocol
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_set_protocol(protocol: string) -> json
tags: ["vpn","nordvpn","protocol","nordlynx","wireguard","openvpn","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_set_protocol.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 41343a8927e771c3b6b46241a2b813a900b05f20c821373de8faa403ad5019f0
description:
  Cambia el protocolo de NordVPN entre NordLynx (WireGuard) y OpenVPN. NordLynx recomendado por velocidad.

example:
  ```bash
  source nordvpn_set_protocol.sh

  nordvpn_set_protocol NordLynx
  # {"ok":true,"protocol":"NordLynx"}

  nordvpn_set_protocol OpenVPN
  # {"ok":true,"protocol":"OpenVPN"}
  ```

notes:
  NordLynx es WireGuard wrapeado por NordVPN — mas rapido y moderno. OpenVPN es mas compatible con redes restrictivas. El cambio de protocolo requiere reconectar si hay una conexion activa.

code:
  # nordvpn_set_protocol
  # --------------------
  # Cambia el protocolo de NordVPN (NordLynx o OpenVPN).
  # NordLynx = WireGuard (recomendado por velocidad).
  #
  # USO (sourced):
  #   source nordvpn_set_protocol.sh
  #   nordvpn_set_protocol NordLynx
  #   nordvpn_set_protocol OpenVPN

  nordvpn_set_protocol() {
      local protocol="${1:?nordvpn_set_protocol: se requiere protocolo (NordLynx|OpenVPN)}"

      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      case "$protocol" in
          NordLynx|nordlynx|NORDLYNX) protocol="NordLynx" ;;
          OpenVPN|openvpn|OPENVPN)     protocol="OpenVPN" ;;
          *)
              echo "{\"ok\":false,\"error\":\"protocolo invalido: $protocol (usar NordLynx o OpenVPN)\"}"
              return 1
              ;;
      esac

      local output
      output=$(nordvpn set protocol "$protocol" 2>&1)
      local rc=$?

      if [ $rc -eq 0 ] || echo "$output" | grep -qi "already set\|successfully"; then
          echo "{\"ok\":true,\"protocol\":\"$protocol\"}"
      else
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"protocol","desc":"protocolo a usar: NordLynx (WireGuard) u OpenVPN"}],"output":"JSON con ok y protocol confirmado"}


id: nordvpn_status_bash_infra
name: nordvpn_status
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: nordvpn_status() -> json
tags: ["vpn","nordvpn","status","infra","network"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/nordvpn_status.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 75dd5ab1ac03ac7fed4abb7dfa5edb55b723e2b702306ba4c178023b3041eb5d
params_schema: {"output":"JSON con estado de VPN: ok, connected, status, hostname, ip, country, city, etc"}
description:
  Obtiene estado actual de NordVPN como JSON estructurado. Incluye servidor, IP, pais, protocolo y estado de conexion.

example:
  ```bash
  source nordvpn_status.sh

  nordvpn_status
  # {"ok":true,"connected":true,"status":"Connected","hostname":"es42.nordvpn.com","ip":"185.x.x.x","country":"Spain","city":"Madrid","current_technology":"NordLynx","current_protocol":"nordlynx","transfer":"1.2 MiB received, 500 KiB sent","uptime":"5 minutes 32 seconds"}

  # Desconectado:
  # {"ok":true,"connected":false,"status":"Disconnected"}
  ```

notes:
  Parsea la salida clave-valor de `nordvpn status` eliminando codigos ANSI. Los campos disponibles dependen del estado de conexion — cuando esta desconectado solo devuelve status y connected.

code:
  # nordvpn_status
  # --------------
  # Obtiene el estado actual de NordVPN como JSON estructurado.
  # Parsea la salida clave-valor de `nordvpn status` a campos JSON.
  #
  # USO (sourced):
  #   source nordvpn_status.sh
  #   nordvpn_status

  nordvpn_status() {
      if ! command -v nordvpn &>/dev/null; then
          echo '{"ok":false,"error":"nordvpn no instalado"}' >&2
          return 1
      fi

      local output
      output=$(nordvpn status 2>&1)
      local rc=$?

      if [ $rc -ne 0 ]; then
          echo "{\"ok\":false,\"error\":$(echo "$output" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))' 2>/dev/null || echo "\"$output\"")}"
          return 1
      fi

      # Parsear output clave: valor a JSON con python3
      echo "$output" | python3 -c '
  import sys, json, re

  lines = sys.stdin.read().strip().split("\n")
  data = {"ok": True}
  for line in lines:
      line = re.sub(r"\x1b\[[0-9;]*m", "", line).strip()
      line = line.lstrip("- ")
      if ":" in line:
          key, _, val = line.partition(":")
          key = key.strip().lower().replace(" ", "_")
          val = val.strip()
          if key == "status":
              data["connected"] = val.lower() == "connected"
          data[key] = val
  print(json.dumps(data))
  '
  }


id: notebook_to_pdf_bash_infra
name: notebook_to_pdf
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: notebook_to_pdf(project_dir: string, [pattern: string], [output_dir: string]) -> string
tags: ["jupyter","notebook","pdf","export","nbconvert","playwright"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/notebook_to_pdf.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 25a8d0d0e83b68ff24096b322ad4d3f3d39ac3a5ace80116deae60bb1e9f4965
description:
  Convierte notebooks Jupyter a PDF usando nbconvert webpdf con chromium. Lista los PDFs generados al finalizar.

example:
  ```bash
  source notebook_to_pdf.sh

  # Con defaults (notebooks/*.ipynb -> notebooks/pdf/)
  notebook_to_pdf /home/lucas/analysis/finanzas

  # Con pattern y output_dir custom
  notebook_to_pdf /home/lucas/analysis/finanzas "notebooks/01_*.ipynb" "exports/pdf/"
  ```

notes:
  Requiere nbconvert y playwright con chromium instalados (usa `install_nbconvert` antes). Usa el venv del proyecto directamente (`.venv/bin/jupyter`). El output_dir es relativo a project_dir. Imprime los PDFs generados con sus rutas al finalizar. Falla si no se genera ningun PDF.

code:
  # notebook_to_pdf
  # ----------------
  # Convierte notebooks Jupyter a PDF usando nbconvert webpdf.
  # Requiere nbconvert y playwright con chromium instalados.
  #
  # USO (sourced):
  #   source notebook_to_pdf.sh
  #   notebook_to_pdf /path/to/project
  #   notebook_to_pdf /path/to/project "notebooks/*.ipynb" "notebooks/pdf/"

  notebook_to_pdf() {
      local project_dir="$1"
      local pattern="${2:-notebooks/*.ipynb}"
      local output_dir="${3:-notebooks/pdf/}"

      if [ -z "$project_dir" ]; then
          echo "notebook_to_pdf: se requiere project_dir" >&2
          return 1
      fi

      if [ ! -d "$project_dir/.venv" ]; then
          echo "notebook_to_pdf: no existe .venv en $project_dir" >&2
          return 1
      fi

      # Crear directorio de salida si no existe
      mkdir -p "$project_dir/$output_dir"

      # Convertir notebooks a PDF con nbconvert webpdf
      # nbconvert puede retornar exit != 0 por warnings de validacion JSON
      # que no impiden la generacion del PDF, asi que ignoramos el exit code
      # y verificamos que los PDFs se hayan generado
      local nbconvert_output
      nbconvert_output=$(cd "$project_dir" && \
          .venv/bin/jupyter nbconvert \
              --to webpdf \
              --allow-chromium-download \
              --output-dir="$output_dir" \
              $pattern 2>&1) || true

      echo "$nbconvert_output"

      # Listar PDFs generados
      echo ""
      echo "PDFs generados en ${project_dir}/${output_dir}:"
      local pdf_count=0
      while IFS= read -r -d '' pdf; do
          echo "  $pdf"
          pdf_count=$((pdf_count + 1))
      done < <(find "$project_dir/$output_dir" -name "*.pdf" -print0 2>/dev/null)

      if [ "$pdf_count" -eq 0 ]; then
          echo "  (ninguno encontrado — nbconvert pudo haber fallado)" >&2
          echo "$nbconvert_output" >&2
          return 1
      fi

      echo "  Total: $pdf_count PDFs"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio raíz del proyecto con venv y notebooks"},{"name":"pattern","desc":"glob de notebooks a convertir (default: notebooks/*.ipynb)"},{"name":"output_dir","desc":"directorio destino para PDFs relativo a project_dir (default: notebooks/pdf/)"}],"output":"lista de PDFs generados con sus rutas"}


id: pass_delete_bash_infra
name: pass_delete
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_delete(entry: string) -> void
description: Elimina un secreto del password store (pass).
tags: ["pass","secret","credential","delete"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source pass_delete.sh
  pass_delete agentes/viejo-token
  ```

tests: ["elimina entrada de test","falla con entrada inexistente"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_delete.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Usa `pass rm -f` para eliminar sin prompt de confirmacion.
content_hash: a8befe189a97a3c3f9c5b6284412886f11a77ec52acdaf51c52a181b72dd02c9
code:
  # pass_delete
  # -----------
  # Elimina un secreto del password store.
  # Sale con exit code 1 si la entrada no existe o pass falla.
  #
  # USO (sourced):
  #   source pass_delete.sh
  #   pass_delete agentes/viejo-token

  pass_delete() {
      local entry="$1"

      if [ -z "$entry" ]; then
          echo "pass_delete: se requiere nombre de entrada" >&2
          return 1
      fi

      if ! pass rm -f "$entry" >/dev/null 2>&1; then
          echo "pass_delete: fallo al eliminar '$entry'" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"entry","desc":"ruta de entrada en el password store (ej: agentes/token)"}],"output":"sin salida"}


id: pass_generate_bash_infra
name: pass_generate
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_generate(entry: string, [length: int]) -> string
description: Genera un password aleatorio, lo almacena en el password store e imprime el valor generado.
tags: ["pass","secret","credential","generate","random"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["genera password de longitud especifica","default 24 chars"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_generate.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Usa `pass generate -f -n` (force overwrite, no symbols). Default 24 caracteres alfanumericos.
content_hash: 0b51c948b911254b1cf275df02eed3c1e1a5fb990a7accc8e312e592484c352f
example:
  ```bash
  source pass_generate.sh
  new_pass=$(pass_generate agentes/nuevo-servicio 32)
  echo "password generado: $new_pass"
  ```

code:
  # pass_generate
  # -------------
  # Genera un password aleatorio y lo almacena en el password store.
  # Imprime el password generado a stdout.
  # Sale con exit code 1 si pass falla.
  #
  # USO (sourced):
  #   source pass_generate.sh
  #   pass_generate agentes/nuevo-token 32
  #   pass_generate agentes/api-key         # default 24 chars

  pass_generate() {
      local entry="$1"
      local length="${2:-24}"

      if [ -z "$entry" ]; then
          echo "pass_generate: se requiere nombre de entrada" >&2
          return 1
      fi

      local output
      output=$(pass generate -f -n "$entry" "$length" 2>&1)
      if [ $? -ne 0 ]; then
          echo "pass_generate: fallo al generar '$entry': $output" >&2
          return 1
      fi

      # pass generate imprime ANSI escape codes + header + password
      # Extraer ultima linea y limpiar escape codes
      echo "$output" | tail -1 | sed 's/\x1b\[[0-9;]*m//g'
  }

params_schema:
  {"params":[{"name":"entry","desc":"ruta de entrada en el password store"},{"name":"length","desc":"longitud del password (default: 24 caracteres)"}],"output":"password generado en texto plano"}


id: pass_get_bash_infra
name: pass_get
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_get(entry: string) -> string
description: Lee un secreto del password store (pass) y lo imprime a stdout.
tags: ["pass","secret","credential","get"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["lee entrada existente","falla con entrada inexistente"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_get.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7845238d9c7859d3768a00d30f5664eefbbbcd27d8c25954d19f3f419a0d0dd3
example:
  ```bash
  source pass_get.sh
  token=$(pass_get agentes/dataforge-token)
  export GITEA_TOKEN="$token"
  ```

notes:
  Usa `pass show` internamente. Requiere GPG key desbloqueada. No imprime newline final (usa printf %s).

code:
  # pass_get
  # --------
  # Lee un secreto del password store y lo imprime a stdout.
  # Sale con exit code 1 si la entrada no existe o pass falla.
  #
  # USO (sourced):
  #   source pass_get.sh
  #   token=$(pass_get agentes/dataforge-token)

  pass_get() {
      local entry="$1"

      if [ -z "$entry" ]; then
          echo "pass_get: se requiere nombre de entrada" >&2
          return 1
      fi

      local value
      value=$(pass show "$entry" 2>/dev/null)
      if [ $? -ne 0 ]; then
          echo "pass_get: no se pudo leer '$entry'" >&2
          return 1
      fi

      printf '%s' "$value"
  }

params_schema:
  {"params":[{"name":"entry","desc":"ruta de entrada en el password store"}],"output":"valor del secreto en texto plano"}


id: pass_list_bash_infra
name: pass_list
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_list([prefix: string]) -> json
description: Lista entradas del password store como JSON array. Filtra opcionalmente por prefijo.
tags: ["pass","secret","credential","list"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["lista todas las entradas","filtra por prefijo"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_list.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 003a2c5146c97ca46821d37d0ec40037e042c531b420ba0956dbbc3ef2b51572
example:
  ```bash
  source pass_list.sh
  entries=$(pass_list agentes)
  # ["dataforge-token","egutierrez-token","gitea-url"]
  ```

notes:
  Parsea el output tree de `pass ls` y lo convierte a JSON array. Cada entrada es un string con el nombre relativo al prefijo.

code:
  # pass_list
  # ---------
  # Lista entradas del password store como JSON array.
  # Opcionalmente filtra por prefijo.
  # Sale con exit code 1 si pass falla.
  #
  # USO (sourced):
  #   source pass_list.sh
  #   pass_list                  # todas las entradas
  #   pass_list agentes          # solo bajo agentes/

  pass_list() {
      local prefix="${1:-}"

      local raw
      raw=$(pass ls "$prefix" 2>/dev/null)
      if [ $? -ne 0 ]; then
          echo "pass_list: fallo al listar entradas" >&2
          return 1
      fi

      # Parsear output de pass: extraer nombres limpios (sin tree chars)
      local entries
      entries=$(echo "$raw" | sed 's/[│├└──── ]//g' | sed '/^$/d' | grep -v '^Password' | grep -v '^[[:space:]]*$')

      # Construir JSON array
      printf '['
      local first=true
      while IFS= read -r line; do
          line=$(echo "$line" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
          [ -z "$line" ] && continue
          if [ "$first" = true ]; then
              first=false
          else
              printf ','
          fi
          printf '"%s"' "$line"
      done <<< "$entries"
      printf ']'
  }

params_schema:
  {"params":[{"name":"prefix","desc":"prefijo para filtrar entradas (opcional; ej: agentes)"}],"output":"JSON array de nombres de entradas"}


id: pass_set_bash_infra
name: pass_set
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_set(entry: string, [value: string]) -> void
description: Inserta o sobreescribe un secreto en el password store (pass).
tags: ["pass","secret","credential","set","insert"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
example:
  ```bash
  source pass_set.sh
  pass_set agentes/nuevo-servicio "token-abc123"
  ```

tests: ["inserta valor y lo lee de vuelta","sobreescribe valor existente"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_set.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 8b8b3f80d17a9f3f4ac33e2b1080c1214e6b719aafe6cda8dba1a6536fbbcecc
notes:
  Usa `pass insert -m -f` para forzar sobreescritura sin prompt interactivo. Si no se pasa valor como argumento, lee de stdin.

code:
  # pass_set
  # --------
  # Inserta o sobreescribe un secreto en el password store.
  # Lee el valor de stdin si no se pasa como segundo argumento.
  # Sale con exit code 1 si pass falla.
  #
  # USO (sourced):
  #   source pass_set.sh
  #   pass_set agentes/nuevo-token "mi-token-secreto"
  #   echo "mi-token" | pass_set agentes/nuevo-token

  pass_set() {
      local entry="$1"
      local value="$2"

      if [ -z "$entry" ]; then
          echo "pass_set: se requiere nombre de entrada" >&2
          return 1
      fi

      if [ -n "$value" ]; then
          printf '%s' "$value" | pass insert -m -f "$entry" >/dev/null 2>&1
      else
          pass insert -m -f "$entry" >/dev/null 2>&1
      fi

      if [ $? -ne 0 ]; then
          echo "pass_set: fallo al escribir '$entry'" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"entry","desc":"ruta de entrada en el password store"},{"name":"value","desc":"valor del secreto (opcional; se lee de stdin si no se proporciona)"}],"output":"sin salida"}


id: pass_sync_bash_infra
name: pass_sync
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: pass_sync() -> json
description: Sincroniza el password store con el repositorio git remoto (pull + push).
tags: ["pass","secret","sync","git"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: ["sincroniza con remoto"]
test_file_path: bash/functions/infra/pass_test.sh
file_path: bash/functions/infra/pass_sync.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c1a5fdca7d75b68514f4460c1a2eb61137465659facb4db1dcd14969e94bf793
params_schema: {"output":"JSON con resultados de pull y push"}
example:
  ```bash
  source pass_sync.sh
  result=$(pass_sync)
  # {"pull":"Already up to date.","push":"Everything up-to-date"}
  ```

notes:
  Ejecuta `pass git pull` seguido de `pass git push`. Requiere que el password store tenga un remote git configurado. Retorna JSON con la ultima linea de cada operacion.

code:
  # pass_sync
  # ---------
  # Sincroniza el password store con el repositorio git remoto (pull + push).
  # Sale con exit code 1 si la sincronizacion falla.
  #
  # USO (sourced):
  #   source pass_sync.sh
  #   pass_sync

  pass_sync() {
      local pull_out
      pull_out=$(pass git pull 2>&1)
      if [ $? -ne 0 ]; then
          echo "pass_sync: fallo en git pull: $pull_out" >&2
          return 1
      fi

      local push_out
      push_out=$(pass git push 2>&1)
      if [ $? -ne 0 ]; then
          echo "pass_sync: fallo en git push: $push_out" >&2
          return 1
      fi

      printf '{"pull":"%s","push":"%s"}' \
          "$(echo "$pull_out" | tail -1 | sed 's/"/\\"/g')" \
          "$(echo "$push_out" | tail -1 | sed 's/"/\\"/g')"
  }


id: uv_add_packages_bash_infra
name: uv_add_packages
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: uv_add_packages(project_dir: string, ...packages: string) -> void
tags: ["python","uv","pip","packages","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/uv_add_packages.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f807adf1f48d52b92091c7c273f766d781e94ee95bb84908634f583aa688547c
description:
  Instala paquetes Python en un proyecto usando uv add con fallback a pip. Inicializa pyproject.toml si no existe.

example:
  ```bash
  source uv_add_packages.sh
  uv_add_packages /home/lucas/analysis/finanzas jupyter jupyterlab pandas numpy

  # Solo un paquete
  uv_add_packages . polars
  ```

notes:
  Requiere que el venv ya exista (usa `init_uv_venv` antes). Prefiere uv por velocidad y reproducibilidad (lockfile). Si uv no esta disponible, usa pip del venv directamente.

code:
  # uv_add_packages
  # -----------------
  # Instala paquetes Python en un proyecto con uv add.
  # Inicializa pyproject.toml si no existe.
  # Fallback a pip install si uv no esta disponible.
  #
  # USO (sourced):
  #   source uv_add_packages.sh
  #   uv_add_packages /path/to/project jupyter jupyterlab pandas

  uv_add_packages() {
      local project_dir="$1"
      shift
      local packages=("$@")

      if [ ${#packages[@]} -eq 0 ]; then
          echo "uv_add_packages: se requiere al menos un paquete" >&2
          return 1
      fi

      if [ ! -d "$project_dir/.venv" ]; then
          echo "uv_add_packages: no existe .venv en $project_dir — ejecuta init_uv_venv primero" >&2
          return 1
      fi

      if command -v uv &>/dev/null; then
          # Inicializar pyproject.toml si no existe
          if [ ! -f "$project_dir/pyproject.toml" ]; then
              (cd "$project_dir" && uv init 2>/dev/null) || true
          fi
          (cd "$project_dir" && uv add "${packages[@]}" 2>&1)
      else
          "$project_dir/.venv/bin/pip" install "${packages[@]}" 2>&1
      fi
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto con venv existente"},{"name":"packages","desc":"nombres de paquetes Python a instalar (variadic)"}],"output":"sin salida"}


id: write_claude_jupyter_rules_bash_infra
name: write_claude_jupyter_rules
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_claude_jupyter_rules([project_dir: string]) -> string
tags: ["claude","jupyter","rules","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_claude_jupyter_rules.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 66e06d51635c07b708c2b274aeff6961ce58197db2ac3c66fa3816a0bbdb1030
description:
  Genera o actualiza .claude/CLAUDE.md con reglas para agentes que trabajan con Jupyter: celdas inmutables, programacion funcional, uso de MCP, acceso al fn_registry.

example:
  ```bash
  source write_claude_jupyter_rules.sh
  path=$(write_claude_jupyter_rules /home/lucas/analysis/finanzas)
  echo "Reglas escritas en: $path"
  ```

notes:
  Idempotente: si CLAUDE.md ya contiene las reglas (detecta "JUPYTER HABILITADO"), no las duplica. Si CLAUDE.md existe sin las reglas, las prepend al contenido existente. Incluye instrucciones de acceso al fn_registry via `FN_REGISTRY_ROOT`.

code:
  # write_claude_jupyter_rules
  # ----------------------------
  # Genera .claude/CLAUDE.md con reglas para agentes que trabajan con Jupyter.
  # Si ya existe CLAUDE.md y no tiene las reglas, las prepend.
  #
  # USO (sourced):
  #   source write_claude_jupyter_rules.sh
  #   write_claude_jupyter_rules /path/to/project

  write_claude_jupyter_rules() {
      local project_dir="${1:-.}"
      local claude_dir="${project_dir}/.claude"
      local claude_md="${claude_dir}/CLAUDE.md"

      mkdir -p "$claude_dir"

      # Si ya tiene las reglas, no hacer nada
      if [ -f "$claude_md" ] && grep -q "JUPYTER HABILITADO" "$claude_md" 2>/dev/null; then
          echo "$claude_md"
          return 0
      fi

      local rules
      rules='# JUPYTER HABILITADO EN ESTE ANALISIS

  ## Reglas OBLIGATORIAS para Claude

  ### 1. CODIGO INMUTABLE — NUNCA MODIFICAR CELDAS EXISTENTES
  - **PROHIBIDO** usar NotebookEdit para reemplazar celdas existentes
  - **SIEMPRE** anadir celdas NUEVAS al final del notebook
  - Si hay un error en una celda, crear celda nueva con la correccion
  - El historial de trabajo debe quedar intacto para trazabilidad

  ### 2. PROGRAMACION FUNCIONAL OBLIGATORIA
  - **Funciones puras**: sin efectos secundarios, mismo input -> mismo output
  - **Inmutabilidad**: nunca mutar datos, crear copias transformadas
  - **Composicion**: funciones pequenas que se combinan
  - Preferir: `map`, `filter`, `reduce`, list comprehensions
  - Evitar: loops con mutacion, `global`, modificar argumentos in-place

  ### 3. SIEMPRE usar MCP jupyter para ejecutar codigo Python
  - Las ejecuciones se ven en tiempo real en Jupyter Lab del usuario
  - Compartimos variables y estado del kernel
  - **NUNCA usar bash para ejecutar Python en este analisis**

  ### 4. Verificar Jupyter activo ANTES de ejecutar
  - Si no esta activo: pedir al usuario que ejecute `./run-jupyter-lab.sh`

  ### 5. Gestion de notebooks
  - Notebooks en la carpeta `notebooks/` o subcarpetas
  - Si un notebook tiene >50 celdas, crear uno nuevo
  - Nombrar descriptivamente: `01_exploracion.ipynb`, `02_limpieza.ipynb`

  ### 6. Gestion de Python
  - **SIEMPRE usar `uv`** para gestionar dependencias
  - Anadir paquetes con `uv add nombre_paquete`

  ### 7. Acceso al fn_registry
  - `FN_REGISTRY_ROOT` apunta a la raiz del registry
  - Para importar funciones Python: `sys.path.insert(0, os.path.join(os.environ["FN_REGISTRY_ROOT"], "python", "functions"))`
  - Para consultar registry.db: `sqlite3` o `import sqlite3` con la ruta `$FN_REGISTRY_ROOT/registry.db`

  '

      if [ -f "$claude_md" ]; then
          local existing
          existing=$(cat "$claude_md")
          printf '%s\n%s' "$rules" "$existing" > "$claude_md"
      else
          echo "$rules" > "$claude_md"
      fi

      echo "$claude_md"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del archivo .claude/CLAUDE.md generado o actualizado"}


id: write_jupyter_launcher_bash_infra
name: write_jupyter_launcher
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_jupyter_launcher([project_dir: string]) -> string
tags: ["jupyter","launcher","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_jupyter_launcher.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c59337b64b64deec591f79895e9f25008bf166a7bd38613bf7671f37999eff01
description:
  Genera un script run-jupyter-lab.sh que lanza Jupyter Lab en modo colaborativo con autodeteccion de puerto y token deshabilitado.

example:
  ```bash
  source write_jupyter_launcher.sh
  path=$(write_jupyter_launcher /home/lucas/analysis/finanzas)
  echo "Launcher creado en: $path"

  # Luego en otra terminal:
  # ./run-jupyter-lab.sh [puerto]
  ```

notes:
  El launcher generado:
  - Autodetecta un puerto libre (8888-8899)
  - Guarda el puerto en `.jupyter-port` para que otros procesos lo lean
  - Activa el venv automaticamente
  - Lanza Jupyter Lab en modo `--collaborative` (requiere jupyter-collaboration)
  - Token y password deshabilitados para acceso local

code:
  # write_jupyter_launcher
  # -----------------------
  # Genera un script run-jupyter-lab.sh en el directorio dado.
  # El script lanza Jupyter Lab en modo colaborativo con autodeteccion de puerto.
  #
  # USO (sourced):
  #   source write_jupyter_launcher.sh
  #   write_jupyter_launcher /path/to/project

  write_jupyter_launcher() {
      local project_dir="${1:-.}"
      local launcher="${project_dir}/run-jupyter-lab.sh"

      cat > "$launcher" << 'LAUNCHER'
  #!/bin/bash
  # Jupyter Lab — modo colaborativo con autodeteccion de puerto
  # Generado por write_jupyter_launcher (fn_registry)

  find_free_port() {
      for port in 8888 8889 8890 8891 8892 8893 8894 8895 8896 8897 8898 8899; do
          if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
             ! lsof -i:"$port" >/dev/null 2>&1; then
              echo $port
              return
          fi
      done
      echo 8888
  }

  PORT=${1:-$(find_free_port)}
  cd "$(dirname "$0")"

  echo $PORT > .jupyter-port

  source .venv/bin/activate 2>/dev/null || true

  if ! python -c "import jupyter_collaboration" 2>/dev/null; then
      echo "ERROR: jupyter-collaboration no esta instalado"
      echo "Instala con: uv add jupyter-collaboration"
      exit 1
  fi

  echo "════════════════════════════════════════════════"
  echo "  Jupyter Lab + Colaboracion en puerto $PORT"
  echo "════════════════════════════════════════════════"
  echo ""
  echo "  Abre: http://localhost:$PORT"
  echo "  Ctrl+C para detener"
  echo ""

  jupyter lab \
      --port=$PORT \
      --no-browser \
      --ServerApp.token='' \
      --ServerApp.password='' \
      --ServerApp.disable_check_xsrf=True \
      --ServerApp.allow_origin='*' \
      --ServerApp.root_dir="$(pwd)" \
      --collaborative
  LAUNCHER

      chmod +x "$launcher"
      echo "$launcher"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del script run-jupyter-lab.sh generado"}


id: write_jupyter_registry_kernel_bash_infra
name: write_jupyter_registry_kernel
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_jupyter_registry_kernel([project_dir: string]) -> string
tags: ["jupyter","ipython","kernel","registry","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_jupyter_registry_kernel.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 0ee25c11974d164c5ceb94314112029d7636ab8210b9cfa735dd1688caa5da38
description:
  Genera un script de startup de IPython que autoconfigura FN_REGISTRY_ROOT, sys.path a python/functions del registry, y helpers fn_query/fn_search/fn_code para consultar registry.db desde notebooks.

example:
  ```bash
  source write_jupyter_registry_kernel.sh
  path=$(write_jupyter_registry_kernel /home/lucas/analysis/finanzas)
  echo "Startup creado en: $path"
  ```

  Luego en cualquier notebook del proyecto:

  ```python
  # Ya disponible automaticamente al abrir el notebook:

  # Buscar funciones
  fn_search("finance")

  # Consultar registry.db directamente
  fn_query("SELECT id, signature FROM functions WHERE domain = ?", ("core",))

  # Ver codigo de una funcion
  print(fn_code("filter_list_py_core"))

  # Importar funciones Python del registry directamente
  from core import filter_list, map_list
  from finance import sma, ema, rsi
  ```

notes:
  Genera `.ipython/profile_default/startup/00_fn_registry.py` que se ejecuta automaticamente al iniciar cualquier kernel IPython en el proyecto. No requiere imports manuales — las funciones `fn_query`, `fn_search` y `fn_code` estan disponibles inmediatamente en cada notebook.

  El `sys.path` se configura para que cada dominio de `python/functions/` sea importable directamente (`from core import filter_list`).

  Detecta `FN_REGISTRY_ROOT` buscando la raiz del repo git o subiendo directorios hasta encontrar `registry.db`.

code:
  # write_jupyter_registry_kernel
  # -------------------------------
  # Genera un script de startup de IPython que autoconfigura el acceso
  # al fn_registry en cada notebook: FN_REGISTRY_ROOT, sys.path a
  # python/functions, y un helper fn_query() para consultar registry.db.
  #
  # USO (sourced):
  #   source write_jupyter_registry_kernel.sh
  #   write_jupyter_registry_kernel /path/to/project

  write_jupyter_registry_kernel() {
      local project_dir="${1:-.}"
      local startup_dir="${project_dir}/.ipython/profile_default/startup"
      local registry_root
      registry_root="$(cd "$project_dir" && cd "$(git -C "$project_dir" rev-parse --show-toplevel 2>/dev/null || echo "../..")" && pwd)"

      # Fallback: si no es git, buscar registry.db subiendo directorios
      if [ ! -f "$registry_root/registry.db" ] && [ -f "$project_dir/../../registry.db" ]; then
          registry_root="$(cd "$project_dir/../.." && pwd)"
      fi

      mkdir -p "$startup_dir"

      cat > "${startup_dir}/00_fn_registry.py" << STARTUP
  """
  fn_registry kernel startup
  Autoconfigura acceso al registry en cada notebook.
  Generado por write_jupyter_registry_kernel (fn_registry).
  """
  import os
  import sys
  import sqlite3
  from pathlib import Path

  # ── FN_REGISTRY_ROOT ────────────────────────────────────────
  FN_REGISTRY_ROOT = Path("${registry_root}")
  os.environ["FN_REGISTRY_ROOT"] = str(FN_REGISTRY_ROOT)

  # ── sys.path: importar funciones Python del registry ────────
  _python_functions = FN_REGISTRY_ROOT / "python" / "functions"
  for _domain in sorted(_python_functions.iterdir()) if _python_functions.exists() else []:
      if _domain.is_dir() and not _domain.name.startswith("_"):
          _path = str(_domain)
          if _path not in sys.path:
              sys.path.insert(0, _path)

  # Tambien el directorio padre para imports por dominio: from core import filter_list
  _pf = str(_python_functions)
  if _pf not in sys.path:
      sys.path.insert(0, _pf)

  # ── fn_query: consultar registry.db desde el notebook ───────
  _REGISTRY_DB = FN_REGISTRY_ROOT / "registry.db"

  def fn_query(sql, params=()):
      """Ejecuta una consulta SQL sobre registry.db y retorna las filas.

      Ejemplos:
          fn_query("SELECT id, description FROM functions WHERE domain = ?", ("finance",))
          fn_query("SELECT id FROM functions_fts WHERE functions_fts MATCH ?", ("slice*",))
      """
      if not _REGISTRY_DB.exists():
          raise FileNotFoundError(f"registry.db no encontrado en {_REGISTRY_DB}")
      con = sqlite3.connect(str(_REGISTRY_DB))
      con.row_factory = sqlite3.Row
      try:
          rows = con.execute(sql, params).fetchall()
          return [dict(r) for r in rows]
      finally:
          con.close()

  def fn_search(term):
      """Busca funciones y tipos en el registry por nombre o descripcion.

      Ejemplo:
          fn_search("slice")
          fn_search("finance")
      """
      fts_term = f"name:{term}* OR description:{term}*"
      functions = fn_query(
          "SELECT id, kind, purity, lang, description FROM functions "
          "WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH ?) "
          "ORDER BY name", (fts_term,)
      )
      types = fn_query(
          "SELECT id, algebraic, lang, description FROM types "
          "WHERE id IN (SELECT id FROM types_fts WHERE types_fts MATCH ?) "
          "ORDER BY name", (fts_term,)
      )
      return {"functions": functions, "types": types}

  def fn_code(function_id):
      """Retorna el codigo fuente de una funcion del registry.

      Ejemplo:
          print(fn_code("filter_list_py_core"))
      """
      rows = fn_query("SELECT code FROM functions WHERE id = ?", (function_id,))
      if not rows:
          raise KeyError(f"Funcion no encontrada: {function_id}")
      return rows[0]["code"]

  # ── Mensaje de bienvenida ───────────────────────────────────
  print(f"fn_registry conectado: {FN_REGISTRY_ROOT}")
  print(f"  registry.db: {'OK' if _REGISTRY_DB.exists() else 'NO ENCONTRADO'}")
  print(f"  Python functions: {_pf}")
  print(f"  Helpers: fn_query(), fn_search(), fn_code()")
  STARTUP

      echo "${startup_dir}/00_fn_registry.py"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"}],"output":"ruta del archivo startup generado"}


id: write_mcp_jupyter_config_bash_infra
name: write_mcp_jupyter_config
kind: function
lang: bash
domain: infra
version: 1.0.0
purity: impure
signature: write_mcp_jupyter_config([project_dir: string], [port: int]) -> string
tags: ["mcp","jupyter","config","setup","infra"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/infra/write_mcp_jupyter_config.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ec2747315cff0aa1f460e7b84e6c6a0d48c67005534f729586341256be0ab91e
description:
  Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server apuntando al venv local y puerto dado. Merge con jq si ya existe.

example:
  ```bash
  source write_mcp_jupyter_config.sh
  path=$(write_mcp_jupyter_config /home/lucas/analysis/finanzas 8890)
  echo "Config MCP en: $path"
  ```

notes:
  El MCP se invoca como modulo Python (`python -m jupyter_mcp_server`) usando el python del venv local, nunca una instalacion global. Si `.mcp.json` ya existe y jq esta disponible, hace merge conservando otros servidores MCP. Sin jq, sobrescribe el archivo.

code:
  # write_mcp_jupyter_config
  # -------------------------
  # Genera o actualiza .mcp.json con la configuracion de jupyter-mcp-server.
  # Usa el python del venv local con -m jupyter_mcp_server.server.
  # Configura via env vars (SERVER_URL, TOKEN) — no CLI args.
  # Hace merge si ya existe .mcp.json (requiere jq).
  #
  # USO (sourced):
  #   source write_mcp_jupyter_config.sh
  #   write_mcp_jupyter_config /path/to/project 8888

  write_mcp_jupyter_config() {
      local project_dir="${1:-.}"
      local port="${2:-8888}"
      local mcp_file="${project_dir}/.mcp.json"
      local abs_project
      abs_project="$(cd "$project_dir" && pwd)"

      local python_bin="${abs_project}/.venv/bin/python"
      if [ ! -f "$python_bin" ]; then
          echo "write_mcp_jupyter_config: python no encontrado en ${python_bin}" >&2
          return 1
      fi

      # Verificar que el modulo esta instalado
      if ! "$python_bin" -c "import jupyter_mcp_server" 2>/dev/null; then
          echo "write_mcp_jupyter_config: jupyter_mcp_server no instalado en el venv" >&2
          return 1
      fi

      local new_config
      new_config=$(cat << EOF
  {
    "mcpServers": {
      "jupyter": {
        "command": "${python_bin}",
        "args": ["-m", "jupyter_mcp_server.server"],
        "env": {
          "SERVER_URL": "http://localhost:${port}",
          "TOKEN": ""
        }
      }
    }
  }
  EOF
  )

      if [ -f "$mcp_file" ] && command -v jq &>/dev/null; then
          # Merge conservando otros servidores MCP
          jq -s '.[0] * {mcpServers: ((.[0].mcpServers // {}) * (.[1].mcpServers // {}))}' \
              "$mcp_file" <(echo "$new_config") > "${mcp_file}.tmp"
          mv "${mcp_file}.tmp" "$mcp_file"
      else
          echo "$new_config" > "$mcp_file"
      fi

      echo "$mcp_file"
  }

params_schema:
  {"params":[{"name":"project_dir","desc":"directorio del proyecto Jupyter (default: directorio actual)"},{"name":"port","desc":"puerto Jupyter (default: detectado automáticamente)"}],"output":"ruta del archivo .mcp.json generado o actualizado"}


id: audit_registry_paths_bash_pipelines
name: audit_registry_paths
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: audit_registry_paths([output_file: string]) -> void
tags: ["registry","audit","validation","paths","launcher","pipeline","bash"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/audit_registry_paths.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 6b6fc9847fe3e4a13cb613cb0e725abce527c710b7c2c4e444e43a70c3aee39c
description:
  Audita file_path de todas las functions y types en registry.db, verifica que cada ruta apunte a un archivo existente en disco, y genera un txt con las rutas rotas para que agentes puedan corregirlas.

uses_functions:
  ["assert_command_exists_bash_shell","assert_file_exists_bash_shell","validate_registry_paths_bash_shell","report_execution_json_bash_shell","exit_with_status_bash_shell"]

example:
  ```bash
  # Con default (genera broken_paths.txt en la raiz)
  ./bash/functions/pipelines/audit_registry_paths.sh

  # Con ruta personalizada
  ./bash/functions/pipelines/audit_registry_paths.sh /tmp/broken.txt

  # Desde fn run (pipeline launcher)
  fn run audit_registry_paths
  ```

notes:
  El archivo de salida es consumible por agentes: cada linea tiene el ID de la funcion/tipo y el file_path que necesita correccion. El agente puede leer el .md correspondiente, encontrar el archivo real en disco, y actualizar el campo `file_path`.

documentation:
  ## Flujo

  1. `assert_command_exists` — verifica que `sqlite3` esta disponible
  2. `assert_file_exists` — verifica que `registry.db` existe y reporta su tamano
  3. `validate_registry_paths` (functions) — itera todas las functions, verifica cada file_path
  4. `validate_registry_paths` (types) — itera todos los types, verifica cada file_path
  5. Genera `broken_paths.txt` con formato legible para agentes
  6. `report_execution_json` — imprime JSON de ejecucion a stdout
  7. `exit_with_status` — exit code segun resultado

  ## Formato de salida

  ```
  # Broken file_path entries in registry.db
  # Generated: 2026-04-03T10:00:00Z
  # Total: 11 broken paths
  #
  # Format: id | file_path (in .md) | domain | table
  # ---

  ## Functions (11)

  cdp_click_go_browser | functions/infra/cdp_click.go | browser | functions
  ```

code:
  #!/usr/bin/env bash
  # audit_registry_paths
  # --------------------
  # Audita file_path de functions y types en registry.db.
  # Genera un txt con las rutas rotas para que agentes puedan arreglarlas.
  #
  # Compone: assert_command_exists + assert_file_exists +
  #          validate_registry_paths + report_execution_json + exit_with_status
  #
  # USO:
  #   ./audit_registry_paths.sh [OUTPUT_FILE]
  #
  # ARGUMENTOS (opcionales):
  #   OUTPUT_FILE   Ruta del archivo de salida
  #                 Default: $REGISTRY_ROOT/broken_paths.txt

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/validate_registry_paths.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/report_execution_json.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/exit_with_status.sh"

  OUTPUT_FILE="${1:-$REGISTRY_ROOT/broken_paths.txt}"
  DB_PATH="$REGISTRY_ROOT/registry.db"

  STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  START_MS=$(date +%s%3N)
  STEPS_FILE=$(mktemp)
  trap 'rm -f "$STEPS_FILE"' EXIT

  ok_steps=0
  failed_steps=0

  log_step() {
      local name="$1" action="$2" status="$3" elapsed="$4" output="${5:-}" error="${6:-}"
      printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$name" "$action" "$status" "$elapsed" "$output" "$error" >> "$STEPS_FILE"
      if [[ "$status" == "ok" ]]; then ok_steps=$((ok_steps + 1)); else failed_steps=$((failed_steps + 1)); fi
  }

  # Paso 1: verificar sqlite3
  step_start=$(date +%s%3N)
  if assert_command_exists sqlite3; then
      log_step "assert_command_exists" "check sqlite3" "ok" $(( $(date +%s%3N) - step_start ))
  else
      log_step "assert_command_exists" "check sqlite3" "error" $(( $(date +%s%3N) - step_start )) "" "sqlite3 not found"
      ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      DURATION=$(( $(date +%s%3N) - START_MS ))
      set +e; report_execution_json "audit_registry_paths" "failure" 1 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"; set -e
      exit 1
  fi

  # Paso 2: verificar registry.db
  step_start=$(date +%s%3N)
  if db_size=$(assert_file_exists "$DB_PATH"); then
      log_step "assert_file_exists" "check registry.db" "ok" $(( $(date +%s%3N) - step_start )) "${db_size} bytes"
  else
      log_step "assert_file_exists" "check registry.db" "error" $(( $(date +%s%3N) - step_start )) "" "registry.db not found"
      ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
      DURATION=$(( $(date +%s%3N) - START_MS ))
      set +e; report_execution_json "audit_registry_paths" "failure" 1 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"; set -e
      exit 1
  fi

  # Paso 3: validar functions
  step_start=$(date +%s%3N)
  fn_broken=$(validate_registry_paths "$DB_PATH" functions "$REGISTRY_ROOT")
  fn_count=$(printf '%s' "$fn_broken" | grep -c . || true)
  log_step "validate_registry_paths" "check functions" "ok" $(( $(date +%s%3N) - step_start )) "$fn_count broken"

  # Paso 4: validar types
  step_start=$(date +%s%3N)
  ty_broken=$(validate_registry_paths "$DB_PATH" types "$REGISTRY_ROOT")
  ty_count=$(printf '%s' "$ty_broken" | grep -c . || true)
  log_step "validate_registry_paths" "check types" "ok" $(( $(date +%s%3N) - step_start )) "$ty_count broken"

  # Paso 5: generar archivo de salida
  step_start=$(date +%s%3N)
  total_broken=$(( fn_count + ty_count ))

  {
      echo "# Broken file_path entries in registry.db"
      echo "# Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
      echo "# Total: $total_broken broken paths"
      echo "#"
      echo "# Format: id | file_path (in .md) | domain | table"
      echo "# ---"
      if [[ $total_broken -eq 0 ]]; then
          echo "# All paths are valid."
      else
          if [[ -n "$fn_broken" ]]; then
              echo ""
              echo "## Functions ($fn_count)"
              printf '%s\n' "$fn_broken" | while IFS=$'\t' read -r id fp domain table; do
                  echo "$id | $fp | $domain | $table"
              done
          fi
          if [[ -n "$ty_broken" ]]; then
              echo ""
              echo "## Types ($ty_count)"
              printf '%s\n' "$ty_broken" | while IFS=$'\t' read -r id fp domain table; do
                  echo "$id | $fp | $domain | $table"
              done
          fi
      fi
  } > "$OUTPUT_FILE"

  log_step "write_output" "generate $OUTPUT_FILE" "ok" $(( $(date +%s%3N) - step_start )) "$total_broken entries"

  # Reporte final
  ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
  DURATION=$(( $(date +%s%3N) - START_MS ))
  total_steps=$(( ok_steps + failed_steps ))

  if [[ $total_broken -eq 0 ]]; then
      status="success"
  else
      status="partial"
  fi

  set +e
  report_execution_json "audit_registry_paths" "$status" 0 "$STARTED_AT" "$ENDED_AT" "$DURATION" "$STEPS_FILE"
  set -e

  echo ""
  echo "--- Results ---"
  echo "Broken paths: $total_broken"
  echo "Output: $OUTPUT_FILE"

  exit_with_status "$total_steps" "$ok_steps" "$failed_steps" > /dev/null

params_schema:
  {"params":[{"name":"output_file","desc":"archivo de salida con rutas rotas (default: broken_paths.txt en raíz)"}],"output":"sin salida directa; genera archivo con rutas inválidas"}


id: export_analysis_pdfs_bash_pipelines
name: export_analysis_pdfs
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: export_analysis_pdfs(nombre: string, [pattern: string]) -> void
tags: ["pipeline","jupyter","pdf","export","nbconvert","launcher"]
uses_functions: ["assert_command_exists_bash_shell","install_nbconvert_bash_infra","notebook_to_pdf_bash_infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/export_analysis_pdfs.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 547c15c27db0fa782bf3a11e72024e9a2634652f25e17f5790cb3378f3fc39bd
description:
  Exporta todos los notebooks de un analisis Jupyter a PDF. Instala nbconvert y playwright automaticamente si no estan presentes.

example:
  ```bash
  # Exportar todos los notebooks de un analisis
  ./export_analysis_pdfs.sh finanzas

  # Con pattern especifico
  ./export_analysis_pdfs.sh ml "notebooks/01_*.ipynb"

  # Via fn run
  fn run export_analysis_pdfs finanzas
  fn run export_analysis_pdfs ml "notebooks/01_*.ipynb"
  ```

notes:
  El analysis debe existir previamente en `analysis/{nombre}/` con un venv inicializado. Los PDFs se generan en `analysis/{nombre}/notebooks/pdf/` por defecto. El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecucion.

documentation:
  ## Flujo

  1. `assert_command_exists uv` — verifica que uv esta disponible
  2. `install_nbconvert` — instala nbconvert y playwright con chromium (idempotente)
  3. `notebook_to_pdf` — convierte notebooks al patron indicado a PDF en `notebooks/pdf/`

code:
  #!/usr/bin/env bash
  # export_analysis_pdfs
  # ---------------------
  # Pipeline que exporta todos los notebooks de un analisis a PDF.
  # Compone: assert_command_exists + install_nbconvert + notebook_to_pdf
  #
  # USO:
  #   ./export_analysis_pdfs.sh <nombre_analysis> [pattern]
  #
  # EJEMPLOS:
  #   ./export_analysis_pdfs.sh finanzas
  #   ./export_analysis_pdfs.sh ml "notebooks/01_*.ipynb"

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  # Source funciones atomicas
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/install_nbconvert.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/notebook_to_pdf.sh"

  # ── Argumentos ──────────────────────────────────────────────

  NOMBRE="${1:-}"
  if [ -z "$NOMBRE" ]; then
      echo "Uso: $0 <nombre_analysis> [pattern]" >&2
      echo "  Ejemplo: $0 finanzas" >&2
      echo "  Ejemplo: $0 ml 'notebooks/01_*.ipynb'" >&2
      exit 1
  fi
  shift
  PATTERN="${1:-notebooks/*.ipynb}"

  ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}"

  # Verificar que el analysis existe
  if [ ! -d "$ANALYSIS_DIR" ]; then
      echo "Error: analysis '${NOMBRE}' no existe en ${ANALYSIS_DIR}" >&2
      exit 1
  fi

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  EXPORT ANALYSIS PDFs: ${NOMBRE}"
  echo "  Directorio: ${ANALYSIS_DIR}"
  echo "════════════════════════════════════════════════════════════"
  echo ""

  # ── 1. Verificar herramientas ───────────────────────────────

  echo "[1/3] Verificando herramientas..."
  assert_command_exists uv
  echo "  OK"

  # ── 2. Instalar nbconvert + playwright ──────────────────────

  echo "[2/3] Instalando dependencias de exportacion..."
  install_nbconvert "$ANALYSIS_DIR"
  echo "  OK"

  # ── 3. Convertir notebooks a PDF ────────────────────────────

  echo "[3/3] Convirtiendo notebooks a PDF..."
  notebook_to_pdf "$ANALYSIS_DIR" "$PATTERN"

  # ── Resumen ─────────────────────────────────────────────────

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  EXPORT COMPLETADO"
  echo "════════════════════════════════════════════════════════════"

params_schema:
  {"params":[{"name":"nombre","desc":"nombre del análisis en analysis/{nombre}/"},{"name":"pattern","desc":"glob de notebooks a exportar (default: notebooks/*.ipynb)"}],"output":"sin salida directa; genera PDFs en notebooks/pdf/"}


id: init_jupyter_analysis_bash_pipelines
name: init_jupyter_analysis
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
signature: init_jupyter_analysis(nombre: string, [...paquetes_extra: string]) -> void
tags: ["jupyter","analysis","setup","pipeline","bash","launcher"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/init_jupyter_analysis.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 9adee852b86ced3705792530a50eebe4a7770de3c37002fc94b8a33dfc77a674
description:
  Inicializa un analisis Jupyter completo en analysis/{nombre}/ con venv, paquetes, launcher, MCP y reglas para agentes Claude. Acepta paquetes extra opcionales.

uses_functions:
  ["assert_command_exists_bash_shell","find_free_port_bash_shell","init_uv_venv_bash_infra","uv_add_packages_bash_infra","write_jupyter_launcher_bash_infra","write_mcp_jupyter_config_bash_infra","write_claude_jupyter_rules_bash_infra","write_jupyter_registry_kernel_bash_infra"]

example:
  ```bash
  # Analisis basico
  ./init_jupyter_analysis.sh finanzas

  # Con paquetes extra
  ./init_jupyter_analysis.sh duckdb polars duckdb
  ./init_jupyter_analysis.sh ml scikit-learn torch

  # Via fn run
  fn run init_jupyter_analysis finanzas
  fn run init_jupyter_analysis ml scikit-learn torch
  ```

notes:
  Cada analisis es independiente (propio venv, propio Jupyter, propio MCP). Mismo patron que `apps/` pero para exploraciones no reutilizables.

  El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecucion.

  Paquetes base siempre incluidos: jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib. Los paquetes extra se añaden a estos.

documentation:
  ## Flujo

  1. `assert_command_exists` — verifica que uv o python3 estan disponibles
  2. Crea estructura `analysis/{nombre}/notebooks/` y `analysis/{nombre}/data/`
  3. `init_uv_venv` — crea venv en `analysis/{nombre}/.venv/`
  4. `uv_add_packages` — instala jupyter, jupyterlab, jupyter-collaboration, jupyter-mcp-server, pandas, numpy, matplotlib + extras
  5. `write_jupyter_launcher` — genera `run-jupyter-lab.sh` con modo colaborativo
  6. `find_free_port` + `write_mcp_jupyter_config` — detecta puerto libre y genera `.mcp.json`
  7. `write_claude_jupyter_rules` — genera `.claude/CLAUDE.md` con reglas de agente
  8. `write_jupyter_registry_kernel` — genera IPython startup con `fn_query`, `fn_search`, `fn_code` y acceso a `python/functions/`

code:
  #!/usr/bin/env bash
  # init_jupyter_analysis
  # ----------------------
  # Inicializa un analisis Jupyter completo en analysis/{nombre}/.
  # Compone: assert_command_exists + find_free_port + init_uv_venv +
  #          uv_add_packages + write_jupyter_launcher +
  #          write_mcp_jupyter_config + write_claude_jupyter_rules +
  #          write_jupyter_registry_kernel
  #
  # USO:
  #   ./init_jupyter_analysis.sh <nombre> [paquetes_extra...]
  #
  # EJEMPLOS:
  #   ./init_jupyter_analysis.sh finanzas
  #   ./init_jupyter_analysis.sh duckdb polars duckdb
  #   ./init_jupyter_analysis.sh ml scikit-learn torch

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  # Source funciones atomicas
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/find_free_port.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/init_uv_venv.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/uv_add_packages.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_launcher.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_mcp_jupyter_config.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_claude_jupyter_rules.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/write_jupyter_registry_kernel.sh"

  # ── Argumentos ──────────────────────────────────────────────

  NOMBRE="${1:-}"
  if [ -z "$NOMBRE" ]; then
      echo "Uso: $0 <nombre> [paquetes_extra...]" >&2
      echo "  Ejemplo: $0 finanzas polars" >&2
      exit 1
  fi
  shift
  EXTRA_PACKAGES=("$@")

  ANALYSIS_DIR="${REGISTRY_ROOT}/analysis/${NOMBRE}"

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  INIT JUPYTER ANALYSIS: ${NOMBRE}"
  echo "  Directorio: ${ANALYSIS_DIR}"
  echo "════════════════════════════════════════════════════════════"
  echo ""

  # ── 1. Verificar herramientas ───────────────────────────────

  echo "[1/8] Verificando herramientas..."
  assert_command_exists uv || assert_command_exists python3
  echo "  OK"

  # ── 2. Crear estructura de carpetas ─────────────────────────

  echo "[2/8] Creando estructura..."
  mkdir -p "$ANALYSIS_DIR/notebooks" "$ANALYSIS_DIR/data"
  echo "  ${ANALYSIS_DIR}/notebooks/"
  echo "  ${ANALYSIS_DIR}/data/"

  # ── 3. Crear venv ───────────────────────────────────────────

  echo "[3/8] Inicializando venv..."
  venv_path=$(init_uv_venv "$ANALYSIS_DIR")
  echo "  $venv_path"

  # ── 4. Instalar paquetes ────────────────────────────────────

  echo "[4/8] Instalando paquetes..."
  BASE_PACKAGES=(jupyter jupyterlab jupyter-collaboration jupyter-mcp-server pandas numpy matplotlib)
  ALL_PACKAGES=("${BASE_PACKAGES[@]}" "${EXTRA_PACKAGES[@]}")
  uv_add_packages "$ANALYSIS_DIR" "${ALL_PACKAGES[@]}"
  echo "  Instalados: ${ALL_PACKAGES[*]}"

  # ── 5. Generar launcher ─────────────────────────────────────

  echo "[5/8] Generando launcher..."
  launcher=$(write_jupyter_launcher "$ANALYSIS_DIR")
  echo "  $launcher"

  # ── 6. Configurar MCP ───────────────────────────────────────

  echo "[6/8] Configurando MCP..."
  port=$(find_free_port 8888 8899)
  mcp_config=$(write_mcp_jupyter_config "$ANALYSIS_DIR" "$port")
  echo "  $mcp_config (puerto: $port)"

  # ── 7. Reglas para agentes ──────────────────────────────────

  echo "[7/8] Escribiendo reglas Claude..."
  rules=$(write_claude_jupyter_rules "$ANALYSIS_DIR")
  echo "  $rules"

  # ── 8. Kernel startup con acceso al registry ────────────────

  echo "[8/8] Configurando kernel con acceso al registry..."
  kernel_startup=$(write_jupyter_registry_kernel "$ANALYSIS_DIR")
  echo "  $kernel_startup"

  # ── Resumen ─────────────────────────────────────────────────

  echo ""
  echo "════════════════════════════════════════════════════════════"
  echo "  ANALISIS '${NOMBRE}' LISTO"
  echo "════════════════════════════════════════════════════════════"
  echo ""
  echo "  Pasos siguientes:"
  echo ""
  echo "  1. En otra terminal:"
  echo "     cd ${ANALYSIS_DIR} && ./run-jupyter-lab.sh"
  echo ""
  echo "  2. Abrir Claude en el analisis:"
  echo "     cd ${ANALYSIS_DIR} && claude"
  echo ""
  echo "  3. Abrir en navegador: http://localhost:${port}"
  echo ""
  echo "  FN_REGISTRY_ROOT=${REGISTRY_ROOT}"
  echo ""

params_schema:
  {"params":[{"name":"nombre","desc":"nombre del análisis a crear"},{"name":"paquetes_extra","desc":"paquetes Python adicionales a instalar (variadic, opcional)"}],"output":"sin salida directa; estructura completa en analysis/{nombre}/"}


id: setup_metabase_volume_bash_pipelines
name: setup_metabase_volume
kind: pipeline
lang: bash
domain: pipelines
version: 1.0.0
purity: impure
tags: ["metabase","docker","setup","launcher","pipeline","bash","infra"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/pipelines/setup_metabase_volume.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 1ef3a6f96c128f50b07f048588f672cf12c2efe339d2bb00438ab7aa0054674f
signature:
  setup_metabase_volume([registry_db_path: string], [container_name: string], [dest_path: string]) -> void

description:
  Copia registry.db al contenedor Docker de Metabase verificando existencia del archivo, disponibilidad de docker, estado del contenedor y coincidencia de tamaños. Todos los argumentos son opcionales con defaults razonables.

uses_functions:
  ["assert_file_exists_bash_shell","assert_command_exists_bash_shell","assert_docker_container_running_bash_infra","docker_cp_file_bash_infra"]

example:
  ```bash
  # Con defaults
  ./functions/pipelines/setup_metabase_volume.sh

  # Con argumentos explícitos
  ./functions/pipelines/setup_metabase_volume.sh \
    /home/lucas/fn_registry/registry.db \
    metabase \
    /registry.db
  ```

notes:
  El pipeline usa `set -euo pipefail` — cualquier fallo en una función individual detiene la ejecución.

  Las funciones individuales se sourcean desde sus rutas en el registry, relativas a `REGISTRY_ROOT` detectado automáticamente desde la ubicación del script.

  Defaults:
  - `REGISTRY_DB_PATH`: `/home/lucas/fn_registry/registry.db`
  - `CONTAINER_NAME`: `metabase`
  - `DEST_PATH`: `/registry.db`

  Nota de persistencia: `docker cp` copia al contenedor en ejecución. Si el contenedor se reinicia, el archivo se pierde. Para persistencia real, montar el directorio como volumen en docker-compose:

  ```yaml
  volumes:
    - /home/lucas/fn_registry:/fn_registry:ro
  ```

  Y usar `--registry-db-path /fn_registry/registry.db`.

documentation:
  ## Flujo

  1. `assert_file_exists` — verifica que `registry.db` existe localmente y obtiene su tamaño
  2. `assert_command_exists` — verifica que `docker` está disponible en PATH
  3. `assert_docker_container_running` — verifica que el contenedor destino está activo; si falla, muestra lista de contenedores activos
  4. `docker_cp_file` — ejecuta `docker cp` y verifica que los tamaños local y remoto coinciden

code:
  #!/usr/bin/env bash
  # setup_metabase_volume
  # ---------------------
  # Copia registry.db al contenedor Docker de Metabase.
  # Compone: assert_file_exists + assert_command_exists +
  #          assert_docker_container_running + docker_cp_file
  #
  # USO:
  #   ./setup_metabase_volume.sh [REGISTRY_DB_PATH] [CONTAINER_NAME] [DEST_PATH]
  #
  # ARGUMENTOS (opcionales, con defaults):
  #   REGISTRY_DB_PATH  Ruta local al registry.db
  #                     Default: /home/lucas/fn_registry/registry.db
  #   CONTAINER_NAME    Nombre del contenedor Docker de Metabase
  #                     Default: metabase
  #   DEST_PATH         Ruta destino dentro del contenedor
  #                     Default: /registry.db

  set -euo pipefail

  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  REGISTRY_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

  source "$REGISTRY_ROOT/bash/functions/shell/assert_file_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/shell/assert_command_exists.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/assert_docker_container_running.sh"
  source "$REGISTRY_ROOT/bash/functions/infra/docker_cp_file.sh"

  REGISTRY_DB_PATH="${1:-/home/lucas/fn_registry/registry.db}"
  CONTAINER_NAME="${2:-metabase}"
  DEST_PATH="${3:-/registry.db}"

  echo "[setup_metabase_volume] Configuracion:"
  echo "  registry.db local : $REGISTRY_DB_PATH"
  echo "  contenedor Docker  : $CONTAINER_NAME"
  echo "  ruta en contenedor : $DEST_PATH"
  echo ""

  # 1. Verificar archivo local
  local_size=$(assert_file_exists "$REGISTRY_DB_PATH")
  echo "[setup_metabase_volume] Archivo local encontrado: ${local_size} bytes"

  # 2. Verificar que docker esta disponible
  assert_command_exists docker
  echo "[setup_metabase_volume] docker disponible en PATH."

  # 3. Verificar que el contenedor esta corriendo
  if ! assert_docker_container_running "$CONTAINER_NAME"; then
      echo "" >&2
      echo "Contenedores activos:" >&2
      docker ps --format "  {{.Names}}\t{{.Status}}\t{{.Image}}" >&2
      echo "" >&2
      echo "Si el contenedor se llama diferente, pasa el nombre como segundo argumento:" >&2
      echo "  ./setup_metabase_volume.sh $REGISTRY_DB_PATH <nombre_contenedor>" >&2
      exit 1
  fi
  echo "[setup_metabase_volume] Contenedor '$CONTAINER_NAME' encontrado y activo."

  # 4. Copiar archivo y verificar tamaños
  echo "[setup_metabase_volume] Copiando $REGISTRY_DB_PATH -> ${CONTAINER_NAME}:${DEST_PATH} ..."
  result=$(docker_cp_file "$REGISTRY_DB_PATH" "$CONTAINER_NAME" "$DEST_PATH")

  remote_size=$(echo "$result" | grep -o '"remote_size":[0-9]*' | cut -d: -f2)
  echo "[setup_metabase_volume] OK Copia completada y verificada."
  echo "[setup_metabase_volume] Tamanio: ${local_size} bytes (local) = ${remote_size} bytes (remoto)"

  echo ""
  echo "---------------------------------------------------------------------"
  echo "registry.db disponible en el contenedor como: $DEST_PATH"
  echo ""
  echo "Ahora ejecuta main.py con:"
  echo ""
  echo "  METABASE_ADMIN_PASSWORD=<password> \\"
  echo "  REGISTRY_DB_PATH=${DEST_PATH} \\"
  echo "  python apps/metabase_registry/main.py"
  echo ""
  echo "O bien:"
  echo ""
  echo "  python apps/metabase_registry/main.py \\"
  echo "    --admin-password <password> \\"
  echo "    --registry-db-path ${DEST_PATH}"
  echo "---------------------------------------------------------------------"

params_schema:
  {"params":[{"name":"registry_db_path","desc":"ruta a registry.db local (default: /home/lucas/fn_registry/registry.db)"},{"name":"container_name","desc":"nombre del contenedor Metabase (default: metabase)"},{"name":"dest_path","desc":"ruta destino dentro del contenedor (default: /registry.db)"}],"output":"sin salida directa; verifica transferencia con coincidencia de tamaños"}


id: assert_command_exists_bash_shell
name: assert_command_exists
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: assert_command_exists(command_name: string) -> void
tags: ["assert","command","exists","validation","shell","bash","path"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/assert_command_exists.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 5c4e73149cf38e36946509c40ea013491b38c991beb820f767fa14edc66fd615
description:
  Verifica que un comando está disponible en el PATH. Sale con exit code 1 si no se encuentra, con mensaje a stderr.

example:
  ```bash
  source functions/shell/assert_command_exists.sh

  assert_command_exists docker
  assert_command_exists jq
  ```

notes:
  Usa `command -v` (POSIX) con redirección `&>/dev/null` para suprimir output. No produce nada a stdout en caso de éxito.

  Output limpio: void en éxito, mensaje a stderr en fallo.

code:
  # assert_command_exists
  # ---------------------
  # Verifica que un comando está disponible en el PATH.
  # No produce output a stdout.
  # Sale con exit code 1 si el comando no se encuentra.
  #
  # USO (sourced):
  #   source assert_command_exists.sh
  #   assert_command_exists docker

  assert_command_exists() {
      local command_name="$1"

      if ! command -v "$command_name" &>/dev/null; then
          echo "assert_command_exists: comando no encontrado en PATH: $command_name" >&2
          return 1
      fi
  }

params_schema:
  {"params":[{"name":"command_name","desc":"nombre del comando a verificar en PATH"}],"output":"sin salida en éxito"}


id: assert_file_exists_bash_shell
name: assert_file_exists
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: assert_file_exists(file_path: string) -> string
tags: ["assert","file","exists","validation","shell","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/assert_file_exists.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 4183e99894fa00ecc4c48143268e35993f5a109593be7edea88ad3b9473ac4b3
description:
  Verifica que un archivo existe en el filesystem. Imprime su tamaño en bytes a stdout. Sale con exit code 1 si el archivo no existe.

example:
  ```bash
  source functions/shell/assert_file_exists.sh

  size=$(assert_file_exists /home/lucas/fn_registry/registry.db)
  echo "Tamaño: $size bytes"
  ```

notes:
  La función se sourcea, no se ejecuta directamente. Usa `stat -c%s` para obtener el tamaño en bytes (compatible con GNU coreutils / Linux).

  Output limpio: solo el número de bytes a stdout. Los errores van a stderr.

  No usa `set -e` internamente — el caller controla el flujo con el exit code de retorno.

code:
  # assert_file_exists
  # ------------------
  # Verifica que un archivo existe en el filesystem.
  # Imprime su tamaño en bytes a stdout.
  # Sale con exit code 1 si el archivo no existe.
  #
  # USO (sourced):
  #   source assert_file_exists.sh
  #   size=$(assert_file_exists /ruta/al/archivo)

  assert_file_exists() {
      local file_path="$1"

      if [ ! -f "$file_path" ]; then
          echo "assert_file_exists: archivo no encontrado: $file_path" >&2
          return 1
      fi

      stat -c%s "$file_path"
  }

params_schema:
  {"params":[{"name":"file_path","desc":"ruta del archivo a verificar"}],"output":"tamaño del archivo en bytes"}


id: exit_with_status_bash_shell
name: exit_with_status
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: pure
signature: exit_with_status(total_steps: int, ok_steps: int, failed_steps: int) -> int
tags: ["execution","status","exit-code","standard"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: bash/functions/shell/exit_with_status.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 3c6882702f826c950f90bf52e72130faf89681f0866e65e8653312e779a3075e
description:
  Calcula el exit code estandar (0=success, 1=failure, 2=partial) a partir de contadores de pasos. Si failed_steps=0 imprime 0 y sale con 0. Si ok_steps=0 imprime 1 y sale con 1. Si hay ambos imprime 2 y sale con 2.

example:
  ```bash
  source bash/functions/shell/exit_with_status.sh

  exit_with_status 5 5 0  # stdout: 0, exit code: 0
  exit_with_status 5 0 5  # stdout: 1, exit code: 1
  exit_with_status 5 3 2  # stdout: 2, exit code: 2
  ```

notes:
  Funcion pura: no realiza I/O de sistema, no modifica estado global, no lee variables de entorno. El argumento `total_steps` se recibe para completitud semantica pero la logica solo depende de `ok_steps` y `failed_steps`. El valor se imprime a stdout ademas de usarse como exit code, de modo que el caller puede capturarlo con `$(exit_with_status ...)` o evaluar directamente con `exit_with_status ... ; echo $?`.

code:
  #!/usr/bin/env bash
  # exit_with_status — calcula el exit code estandar a partir de contadores de pasos

  # exit_with_status <total_steps> <ok_steps> <failed_steps>
  #
  # Calcula el exit code estandar:
  #   0 — todos los pasos exitosos (failed_steps == 0)
  #   1 — todos los pasos fallaron (ok_steps == 0)
  #   2 — resultado parcial (hay ok y failed)
  #
  # Imprime el codigo a stdout y sale con ese codigo.
  exit_with_status() {
      local total_steps="$1"
      local ok_steps="$2"
      local failed_steps="$3"

      local code

      if [[ "$failed_steps" -eq 0 ]]; then
          code=0
      elif [[ "$ok_steps" -eq 0 ]]; then
          code=1
      else
          code=2
      fi

      echo "$code"
      return "$code"
  }

params_schema:
  {"params":[{"name":"total_steps","desc":"número total de pasos ejecutados"},{"name":"ok_steps","desc":"número de pasos exitosos"},{"name":"failed_steps","desc":"número de pasos fallidos"}],"output":"exit code: 0 (éxito), 1 (fallo), o 2 (parcial)"}


id: find_free_port_bash_shell
name: find_free_port
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: find_free_port([start_port: int], [end_port: int]) -> int
tags: ["network","port","shell","utility"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/find_free_port.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: b89bacb32e1a6293e15b43b58a72c92587d3c5b30e1ac95cda12356948561e82
description:
  Busca el primer puerto TCP libre en un rango dado usando ss y lsof. Retorna el numero de puerto a stdout.

example:
  ```bash
  source find_free_port.sh
  port=$(find_free_port 8888 8899)
  echo "Puerto libre: $port"

  # Con defaults (8888-8899)
  port=$(find_free_port)
  ```

notes:
  Usa `ss -tln` como primer intento y `lsof` como fallback. Ambos deben confirmar que el puerto no esta en uso. Si ningun puerto esta libre en el rango, sale con exit code 1.

code:
  # find_free_port
  # ---------------
  # Busca el primer puerto libre en un rango dado.
  # Imprime el puerto encontrado a stdout.
  # Sale con exit code 1 si no encuentra ninguno.
  #
  # USO (sourced):
  #   source find_free_port.sh
  #   port=$(find_free_port 8888 8899)

  find_free_port() {
      local start="${1:-8888}"
      local end="${2:-8899}"

      for ((port=start; port<=end; port++)); do
          if ! ss -tln 2>/dev/null | grep -q ":${port} " && \
             ! lsof -i:"$port" >/dev/null 2>&1; then
              echo "$port"
              return 0
          fi
      done

      echo "find_free_port: no se encontro puerto libre en rango ${start}-${end}" >&2
      return 1
  }

params_schema:
  {"params":[{"name":"start_port","desc":"puerto inicial del rango (default: 8888)"},{"name":"end_port","desc":"puerto final del rango (default: 8899)"}],"output":"número de puerto TCP libre"}


id: report_execution_json_bash_shell
name: report_execution_json
kind: function
lang: bash
domain: shell
version: 2.0.0
purity: pure
tags: ["execution","json","report","standard","shell","pure"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: bash/functions/shell/report_execution_json.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: d069da827fcca273c1d63cc45ddb2f3fccd4240f176f870a45843381f87fa1be
signature:
  report_execution_json(flow_name: string, status: string, exit_code: int, started_at: string, ended_at: string, duration_ms: int, steps_file: string) -> string

description:
  Genera un JSON de reporte de ejecucion siguiendo el estandar fn-registry (docs/execution_standard.md). Recibe los metadatos del flujo y un archivo TSV con resultados de pasos (columnas: name, action, status, elapsed_ms, output, error). Imprime el JSON completo a stdout. Usa jq si esta disponible, con fallback a printf. Funcion pura: sin efectos secundarios ni I/O adicional.

example:
  ```bash
  source bash/functions/shell/report_execution_json.sh

  # Crear archivo de pasos (TSV sin cabecera, 6 columnas)
  cat > /tmp/steps.tsv <<'EOF'
  check_db	command	ok	10	exists
  backup	command	ok	450	done
  push	command	error	2540		remote rejected
  EOF

  report_execution_json \
      "backup_db" "partial" 2 \
      "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 \
      /tmp/steps.tsv
  ```

  Output esperado:

  ```json
  {"name":"backup_db","status":"partial","exit_code":2,"started_at":"2026-04-01T02:00:00Z","ended_at":"2026-04-01T02:00:03Z","duration_ms":3000,"steps_total":3,"steps_ok":2,"steps_failed":1,"steps":[{"name":"check_db","action":"command","status":"ok","elapsed_ms":10,"output":"exists"},{"name":"backup","action":"command","status":"ok","elapsed_ms":450,"output":"done"},{"name":"push","action":"command","status":"error","elapsed_ms":2540,"error":"remote rejected"}]}
  ```

notes:
  Funcion pura: solo lee el archivo de pasos y escribe JSON a stdout, sin efectos secundarios ni I/O adicional mas alla de leer el steps_file.

  Formato TSV: 6 columnas separadas por tabulador real (sin cabecera): name, action, status (ok|error), elapsed_ms, output, error. Los campos output y error pueden estar vacios; se omiten del JSON si no tienen valor, siguiendo el estandar de output estructurado.

  El caller es responsable de calcular status (success|failure|partial) y exit_code (0|1|2) segun las reglas del estandar. Ver exit_with_status_bash_shell para esa logica.

  Compatibilidad dual: con jq construye el JSON de forma robusta (maneja caracteres especiales y saltos de linea en output/error). Sin jq, el fallback con printf escapa backslash, comillas dobles y caracteres de control basicos.

  Puede ejecutarse directamente: `bash report_execution_json.sh <args>`.

code:
  #!/usr/bin/env bash
  # report_execution_json — Genera un JSON de reporte de ejecucion siguiendo el estandar fn-registry.
  #
  # USO (sourced):
  #   source report_execution_json.sh
  #   report_execution_json "backup_db" "partial" 2 \
  #       "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 /tmp/steps.tsv
  #
  # USO (ejecutado directamente):
  #   bash report_execution_json.sh "backup_db" "partial" 2 \
  #       "2026-04-01T02:00:00Z" "2026-04-01T02:00:03Z" 3000 /tmp/steps.tsv
  #
  # FORMATO steps_file (TSV sin cabecera, 6 columnas):
  #   name<TAB>action<TAB>status<TAB>elapsed_ms<TAB>output<TAB>error
  #   Los campos output y error pueden estar vacios.
  #   status valido: ok | error
  #
  # NOTA sobre IFS y tabs: bash trata tab como whitespace en IFS y colapsa
  # campos vacios consecutivos. Se usa 'cut -f N' para parsear cada columna
  # de forma correcta cuando hay campos vacios entre tabs.

  report_execution_json() {
      local flow_name="$1"
      local status="$2"
      local exit_code="$3"
      local started_at="$4"
      local ended_at="$5"
      local duration_ms="$6"
      local steps_file="$7"

      if [[ -z "$flow_name" || -z "$status" || -z "$exit_code" || \
            -z "$started_at" || -z "$ended_at" || -z "$duration_ms" || \
            -z "$steps_file" ]]; then
          echo "report_execution_json: uso: report_execution_json <flow_name> <status> <exit_code> <started_at> <ended_at> <duration_ms> <steps_file>" >&2
          return 1
      fi

      if [[ ! -f "$steps_file" ]]; then
          echo "report_execution_json: archivo de pasos no encontrado: $steps_file" >&2
          return 1
      fi

      if command -v jq >/dev/null 2>&1; then
          _report_execution_json_jq \
              "$flow_name" "$status" "$exit_code" \
              "$started_at" "$ended_at" "$duration_ms" "$steps_file"
      else
          _report_execution_json_printf \
              "$flow_name" "$status" "$exit_code" \
              "$started_at" "$ended_at" "$duration_ms" "$steps_file"
      fi
  }

  # --- Implementacion con jq ---

  _report_execution_json_jq() {
      local flow_name="$1" status="$2" exit_code="$3"
      local started_at="$4" ended_at="$5" duration_ms="$6" steps_file="$7"

      local steps_ok=0 steps_failed=0
      local steps_json="[]"

      while IFS= read -r line; do
          [[ -z "$line" ]] && continue

          local s_name s_action s_status s_elapsed s_output s_error
          s_name=$(printf '%s' "$line" | cut -f1)
          s_action=$(printf '%s' "$line" | cut -f2)
          s_status=$(printf '%s' "$line" | cut -f3)
          s_elapsed=$(printf '%s' "$line" | cut -f4)
          s_output=$(printf '%s' "$line" | cut -f5)
          s_error=$(printf '%s' "$line" | cut -f6)

          [[ -z "$s_name" ]] && continue

          local step_obj
          step_obj=$(jq -n \
              --arg   name     "$s_name" \
              --arg   action   "$s_action" \
              --arg   st       "$s_status" \
              --argjson ms     "${s_elapsed:-0}" \
              --arg   output   "$s_output" \
              --arg   error    "$s_error" \
              '{name: $name, action: $action, status: $st, elapsed_ms: $ms}
               + (if $output != "" then {output: $output} else {} end)
               + (if $error  != "" then {error:  $error}  else {} end)')

          steps_json=$(printf '%s' "$steps_json" | jq --argjson step "$step_obj" '. + [$step]')

          if [[ "$s_status" == "ok" ]]; then
              ((steps_ok++))
          else
              ((steps_failed++))
          fi
      done < "$steps_file"

      local steps_total=$(( steps_ok + steps_failed ))

      jq -n \
          --arg     name         "$flow_name" \
          --arg     st           "$status" \
          --argjson exit_code    "$exit_code" \
          --arg     started_at   "$started_at" \
          --arg     ended_at     "$ended_at" \
          --argjson duration_ms  "$duration_ms" \
          --argjson total        "$steps_total" \
          --argjson ok           "$steps_ok" \
          --argjson failed       "$steps_failed" \
          --argjson steps        "$steps_json" \
          '{
              name:         $name,
              status:       $st,
              exit_code:    $exit_code,
              started_at:   $started_at,
              ended_at:     $ended_at,
              duration_ms:  $duration_ms,
              steps_total:  $total,
              steps_ok:     $ok,
              steps_failed: $failed,
              steps:        $steps
          }'
  }

  # --- Implementacion con printf (fallback sin jq) ---

  _json_escape_rj() {
      local s="$1"
      s="${s//\\/\\\\}"
      s="${s//\"/\\\"}"
      s="${s//$'\n'/\\n}"
      s="${s//$'\r'/\\r}"
      s="${s//$'\t'/\\t}"
      printf '%s' "$s"
  }

  _report_execution_json_printf() {
      local flow_name="$1" status="$2" exit_code="$3"
      local started_at="$4" ended_at="$5" duration_ms="$6" steps_file="$7"

      local steps_ok=0 steps_failed=0
      local steps_parts=()

      while IFS= read -r line; do
          [[ -z "$line" ]] && continue

          local s_name s_action s_status s_elapsed s_output s_error
          s_name=$(printf '%s' "$line" | cut -f1)
          s_action=$(printf '%s' "$line" | cut -f2)
          s_status=$(printf '%s' "$line" | cut -f3)
          s_elapsed=$(printf '%s' "$line" | cut -f4)
          s_output=$(printf '%s' "$line" | cut -f5)
          s_error=$(printf '%s' "$line" | cut -f6)

          [[ -z "$s_name" ]] && continue

          local part
          part=$(printf '{"name":"%s","action":"%s","status":"%s","elapsed_ms":%s' \
              "$(_json_escape_rj "$s_name")" \
              "$(_json_escape_rj "$s_action")" \
              "$(_json_escape_rj "$s_status")" \
              "${s_elapsed:-0}")

          if [[ -n "$s_output" ]]; then
              part+=",\"output\":\"$(_json_escape_rj "$s_output")\""
          fi
          if [[ -n "$s_error" ]]; then
              part+=",\"error\":\"$(_json_escape_rj "$s_error")\""
          fi
          part+="}"

          steps_parts+=("$part")

          if [[ "$s_status" == "ok" ]]; then
              ((steps_ok++))
          else
              ((steps_failed++))
          fi
      done < "$steps_file"

      local steps_total=$(( steps_ok + steps_failed ))

      local steps_array="["
      local first=1
      for part in "${steps_parts[@]}"; do
          if [[ $first -eq 1 ]]; then
              steps_array+="$part"
              first=0
          else
              steps_array+=",$part"
          fi
      done
      steps_array+="]"

      printf '{"name":"%s","status":"%s","exit_code":%s,"started_at":"%s","ended_at":"%s","duration_ms":%s,"steps_total":%s,"steps_ok":%s,"steps_failed":%s,"steps":%s}\n' \
          "$(_json_escape_rj "$flow_name")" \
          "$(_json_escape_rj "$status")" \
          "$exit_code" \
          "$(_json_escape_rj "$started_at")" \
          "$(_json_escape_rj "$ended_at")" \
          "$duration_ms" \
          "$steps_total" \
          "$steps_ok" \
          "$steps_failed" \
          "$steps_array"
  }

  # Permitir ejecucion directa (no solo sourced)
  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      report_execution_json "$@"
  fi

params_schema:
  {"params":[{"name":"flow_name","desc":"nombre del flujo ejecutado"},{"name":"status","desc":"estado final: success, failure, o partial"},{"name":"exit_code","desc":"código de salida: 0, 1, o 2"},{"name":"started_at","desc":"timestamp ISO8601 de inicio"},{"name":"ended_at","desc":"timestamp ISO8601 de fin"},{"name":"duration_ms","desc":"duración total en milisegundos"},{"name":"steps_file","desc":"archivo TSV con resultados de cada paso"}],"output":"JSON estructurado con metadatos de ejecución y array de pasos"}


id: run_steps_bash_shell
name: run_steps
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: run_steps(yaml_file: string, [--strict]) -> string
tags: ["execution","yaml","runner","standard","pipeline"]
uses_functions: ["exit_with_status_bash_shell","report_execution_json_bash_shell"]
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/run_steps.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: c633d74c155f5e8accd1bf4d9b22505e47ce005ad9445740721654fd87aa66aa
description:
  Ejecuta pasos de un YAML generico donde cada step tiene action=command. Lee el YAML con yq, ejecuta cada paso secuencialmente con timeout configurable, captura exit code y output, respeta continue_on_error, y al final reporta JSON estandar a stdout via report_execution_json. Sale con exit code 0 (success), 1 (failure) o 2 (partial). Con --strict mapea partial a failure.

example:
  ```yaml
  # /tmp/test_flow.yaml
  name: test_flow
  steps:
    - name: check
      action: command
      command: "echo hello"
    - name: list
      action: command
      command: "ls /tmp"
      continue_on_error: true
    - name: slow_step
      action: command
      command: "sleep 2"
      timeout_ms: 1000
      continue_on_error: true
  ```

  ```bash
  source bash/functions/shell/run_steps.sh

  run_steps /tmp/test_flow.yaml
  # Imprime JSON a stdout:
  # {
  #   "name": "test_flow",
  #   "status": "partial",
  #   "exit_code": 2,
  #   "started_at": "2026-04-01T10:00:00Z",
  #   "ended_at": "2026-04-01T10:00:01Z",
  #   "duration_ms": 1250,
  #   "steps_total": 3,
  #   "steps_ok": 2,
  #   "steps_failed": 1,
  #   "steps": [
  #     {"name": "check", "action": "command", "status": "ok", "elapsed_ms": 10, "output": "hello"},
  #     {"name": "list", "action": "command", "status": "ok", "elapsed_ms": 15},
  #     {"name": "slow_step", "action": "command", "status": "error", "elapsed_ms": 1001, "error": "timeout: comando excedio 1000ms"}
  #   ]
  # }
  # Sale con exit code 2 (partial)

  run_steps /tmp/test_flow.yaml --strict
  # Igual pero status="failure" y exit code=1
  ```

notes:
  Requiere `yq` (v4+, modo expression `-e`) y `jq` (o la implementacion printf de report_execution_json). Solo soporta `action: command` — otros actions se marcan como error del paso. El campo `timeout_ms` por defecto es 30000 (30s); si el comando excede el timeout, `timeout(1)` sale con exit code 124 y se registra el error correspondiente. El output del comando (stdout+stderr combinados) se sanitiza reemplazando tabuladores y saltos de linea por espacios antes de escribir al TSV temporal. Las funciones `exit_with_status` y `report_execution_json` se cargan via `source` desde el mismo directorio que `run_steps.sh`.

code:
  #!/usr/bin/env bash
  # run_steps — ejecuta pasos de un YAML generico (action=command)

  # run_steps <yaml_file> [--strict]
  #
  # Lee un YAML con la estructura:
  #   name: <run_name>
  #   steps:
  #     - name: <step_name>
  #       action: command
  #       command: "<shell_command>"
  #       continue_on_error: true|false   # opcional, default false
  #       timeout_ms: 30000               # opcional, default 30000
  #
  # Para cada paso de action=command:
  #   - Ejecuta el command con timeout (timeout_ms ms)
  #   - Captura exit code, stdout+stderr y elapsed time
  #   - Si falla y continue_on_error=false → aborta
  #   - Acumula resultados en TSV temporal
  #
  # Al final:
  #   - Llama a report_execution_json para generar JSON a stdout
  #   - Llama a exit_with_status para determinar el exit code
  #
  # --strict: mapea partial (2) a failure (1) en status y exit code
  #
  # Requiere: yq, jq (jq opcional si report_execution_json tiene fallback printf)
  run_steps() {
      local yaml_file="$1"
      local strict=0

      if [[ "${2:-}" == "--strict" ]]; then
          strict=1
      fi

      # --- validaciones previas ---
      if [[ -z "$yaml_file" ]]; then
          echo "run_steps: yaml_file requerido" >&2
          return 1
      fi

      if [[ ! -f "$yaml_file" ]]; then
          echo "run_steps: archivo '$yaml_file' no existe" >&2
          return 1
      fi

      if ! command -v yq &>/dev/null; then
          echo "run_steps: yq no encontrado en PATH" >&2
          return 1
      fi

      # --- cargar funciones del estandar ---
      local script_dir
      script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      # shellcheck source=bash/functions/shell/exit_with_status.sh
      source "$script_dir/exit_with_status.sh"
      # shellcheck source=bash/functions/shell/report_execution_json.sh
      source "$script_dir/report_execution_json.sh"

      # --- leer nombre del run ---
      local run_name
      run_name=$(yq e '.name // "unnamed"' "$yaml_file")

      # --- contar pasos ---
      local step_count
      step_count=$(yq e '.steps | length' "$yaml_file")

      if [[ -z "$step_count" || "$step_count" -eq 0 ]]; then
          echo "run_steps: el YAML no tiene steps" >&2
          return 1
      fi

      # --- archivo TSV temporal para acumular resultados ---
      # columnas: name<TAB>action<TAB>status<TAB>elapsed_ms<TAB>output<TAB>error
      local tsv_file
      tsv_file=$(mktemp /tmp/run_steps_XXXXXX.tsv)
      # shellcheck disable=SC2064
      trap "rm -f '$tsv_file'" EXIT

      local ok_steps=0
      local failed_steps=0
      local started_at
      started_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
      local t_run_start
      t_run_start=$(date +%s%3N)

      # --- iterar sobre cada paso ---
      local i
      for (( i=0; i<step_count; i++ )); do
          local step_name step_action step_command step_continue step_timeout_ms

          step_name=$(yq e ".steps[$i].name // \"step_$i\"" "$yaml_file")
          step_action=$(yq e ".steps[$i].action // \"command\"" "$yaml_file")
          step_command=$(yq e ".steps[$i].command // \"\"" "$yaml_file")
          step_continue=$(yq e ".steps[$i].continue_on_error // false" "$yaml_file")
          step_timeout_ms=$(yq e ".steps[$i].timeout_ms // 30000" "$yaml_file")

          # convertir timeout de ms a segundos enteros (timeout(1) usa segundos)
          local timeout_sec
          timeout_sec=$(( (step_timeout_ms + 999) / 1000 ))

          # solo soportamos action=command
          if [[ "$step_action" != "command" ]]; then
              local err_msg="action '$step_action' no soportada (solo command)"
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "$step_action" "error" "0" "" "$err_msg" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: paso '$step_name' — $err_msg — abortando" >&2
                  break
              fi
              continue
          fi

          if [[ -z "$step_command" ]]; then
              local err_msg="campo 'command' vacio en paso '$step_name'"
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "error" "0" "" "$err_msg" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: $err_msg — abortando" >&2
                  break
              fi
              continue
          fi

          # ejecutar con timeout y capturar output + tiempos
          local t_start t_end elapsed_ms step_output step_exit

          t_start=$(date +%s%3N)
          step_output=$(timeout "$timeout_sec" bash -c "$step_command" 2>&1)
          step_exit=$?
          t_end=$(date +%s%3N)
          elapsed_ms=$(( t_end - t_start ))

          # timeout(1) sale con 124 cuando agota el tiempo
          local step_error=""
          if [[ "$step_exit" -eq 124 ]]; then
              step_error="timeout: comando excedio ${step_timeout_ms}ms"
          fi

          # escapar tabuladores y saltos de linea en el output para el TSV
          local safe_output
          safe_output=$(printf '%s' "$step_output" | tr '\t' ' ' | tr '\n' ' ')

          if [[ "$step_exit" -eq 0 ]]; then
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "ok" "$elapsed_ms" "$safe_output" "" >> "$tsv_file"
              ok_steps=$((ok_steps + 1))
          else
              printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
                  "$step_name" "command" "error" "$elapsed_ms" "$safe_output" "$step_error" >> "$tsv_file"
              failed_steps=$((failed_steps + 1))

              if [[ "$step_continue" != "true" ]]; then
                  echo "run_steps: paso '$step_name' fallo (exit $step_exit) — abortando" >&2
                  break
              fi
          fi
      done

      local t_run_end
      t_run_end=$(date +%s%3N)
      local total_duration_ms=$(( t_run_end - t_run_start ))
      local ended_at
      ended_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
      local total_steps=$(( ok_steps + failed_steps ))

      # --- calcular status y exit code ---
      local status_code
      status_code=$(exit_with_status "$total_steps" "$ok_steps" "$failed_steps")

      local run_status
      case "$status_code" in
          0) run_status="success" ;;
          1) run_status="failure" ;;
          2)
              if [[ "$strict" -eq 1 ]]; then
                  run_status="failure"
                  status_code=1
              else
                  run_status="partial"
              fi
              ;;
          *) run_status="failure"; status_code=1 ;;
      esac

      # --- generar JSON a stdout ---
      report_execution_json \
          "$run_name" \
          "$run_status" \
          "$status_code" \
          "$started_at" \
          "$ended_at" \
          "$total_duration_ms" \
          "$tsv_file"

      return "$status_code"
  }

params_schema:
  {"params":[{"name":"yaml_file","desc":"archivo YAML con especificación de pasos"},{"name":"--strict","desc":"flag opcional para mapear partial a failure"}],"output":"JSON de ejecución con detalles de cada paso"}


id: validate_registry_paths_bash_shell
name: validate_registry_paths
kind: function
lang: bash
domain: shell
version: 1.0.0
purity: impure
signature: validate_registry_paths(db_path: string, table: string, root_dir: string) -> tsv_stdout
tags: ["registry","validation","paths","audit","bash"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: bash/functions/shell/validate_registry_paths.sh
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: eb3eacaa9aaf99f8e96758d349a0d22db148b7ff821d05a5a0ad4d44cfac23b4
description:
  Consulta registry.db y verifica que cada file_path apunte a un archivo existente en disco. Imprime a stdout las rutas rotas en formato TSV (id, file_path, domain, tabla).

example:
  ```bash
  source validate_registry_paths.sh
  validate_registry_paths /home/lucas/fn_registry/registry.db functions /home/lucas/fn_registry

  # Output (TSV):
  # cdp_click_go_browser	functions/infra/cdp_click.go	browser	functions
  ```

notes:
  Impura porque lee el filesystem y la base de datos. No modifica nada — solo reporta.

  La salida TSV es consumible por otros scripts o pipelines. Si no hay rutas rotas, no imprime nada.

code:
  #!/usr/bin/env bash
  # validate_registry_paths
  # -----------------------
  # Consulta registry.db y verifica que cada file_path apunte a un archivo existente.
  # Recibe la ruta a registry.db y la tabla a validar (functions o types).
  # Imprime a stdout las lineas con rutas rotas en formato TSV:
  #   id<TAB>file_path<TAB>domain<TAB>tabla
  # Exit code 0 siempre (es una consulta, no una asercion).
  #
  # USO (sourced):
  #   source validate_registry_paths.sh
  #   validate_registry_paths /ruta/registry.db functions /ruta/raiz
  #
  # USO (directo):
  #   bash validate_registry_paths.sh /ruta/registry.db functions /ruta/raiz

  validate_registry_paths() {
      local db_path="$1"
      local table="$2"
      local root_dir="$3"

      if [[ -z "$db_path" || -z "$table" || -z "$root_dir" ]]; then
          echo "validate_registry_paths: uso: validate_registry_paths <db_path> <table> <root_dir>" >&2
          return 1
      fi

      if [[ "$table" != "functions" && "$table" != "types" ]]; then
          echo "validate_registry_paths: tabla debe ser 'functions' o 'types'" >&2
          return 1
      fi

      local broken=0
      while IFS='|' read -r id fp domain; do
          if [[ ! -f "$root_dir/$fp" ]]; then
              printf '%s\t%s\t%s\t%s\n' "$id" "$fp" "$domain" "$table"
              ((broken++))
          fi
      done < <(sqlite3 "$db_path" "SELECT id, file_path, domain FROM $table ORDER BY id;")

      return 0
  }

  if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
      validate_registry_paths "$@"
  fi

params_schema:
  {"params":[{"name":"db_path","desc":"ruta a registry.db"},{"name":"table","desc":"tabla a validar: functions o types"},{"name":"root_dir","desc":"directorio raíz del repositorio"}],"output":"TSV con id, file_path, domain, tabla de rutas inválidas"}


id: chart_colors_ts_core
name: chart_colors
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: getChartColor(index: number): string
tags: ["chart","color","theme","palette","visualization"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/chart_colors.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 11be49cf7a2a83735ff7a86eb96e60430a6e2f32c4621e572dae7ebbfc90f18a
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/chart-base.tsx
description:
  Paleta de colores para gráficos basada en CSS variables del tema activo. Colores accesibles por índice cíclico.

example:
  ```typescript
  getChartColor(0) // 'hsl(var(--chart-1, 220 70% 50%))'
  getChartColor(7) // 'hsl(var(--chart-3, 30 80% 55%))' — cicla sobre 5 colores
  ```

notes:
  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.

code:
  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]
  }

params_schema:
  {"params":[{"name":"index","desc":"Índice cíclico en la paleta de colores (ciclará sobre 5 colores disponibles)"}],"output":"String con variable CSS de color para gráficos (ej: 'hsl(var(--chart-1, 220 70% 50%))')"}


id: cn_ts_core
name: cn
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: cn(...inputs: ClassValue[]): string
tags: ["css","tailwind","classname","merge","utility"]
uses_functions: []
uses_types: []
returns: []
imports: ["clsx","tailwind-merge"]
tests: []
file_path: frontend/functions/core/cn.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Base de todo el sistema de estilos. Todos los componentes la usan para componer className.
content_hash: 8701922b167c9a76509df4d5318b9e6c8d85bb22bc9dab326e797e5b0ebc41b2
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/lib/utils.ts
description:
  Combina clases CSS con clsx y resuelve conflictos Tailwind con tailwind-merge. Utilidad fundamental para composición de estilos.

example:
  ```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
  ```

code:
  import { clsx, type ClassValue } from "clsx"
  import { twMerge } from "tailwind-merge"

  export function cn(...inputs: ClassValue[]): string {
    return twMerge(clsx(inputs))
  }

params_schema:
  {"params":[{"name":"inputs","desc":"Clases CSS en cualquier formato: strings, arrays, objetos con condiciones booleanas"}],"output":"String con clases CSS combinadas y mergeadas, sin duplicados y conflictos Tailwind resueltos"}


id: format_compact_ts_core
name: format_compact
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: formatCompact(n: number, decimals?: number): string
tags: ["format","number","compact","utility","display"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/format_compact.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
notes: Todas son funciones puras sin dependencias. Útiles en dashboards, KPI cards, tablas y tooltips.
content_hash: c1361e21edf78c98e02ed640215066fe93d3432e575d44fc4eeeca4308b4656b
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).

example:
  ```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'
  ```

code:
  /**
   * 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'
  }

params_schema:
  {"params":[{"name":"n","desc":"Número a formatear de forma compacta"},{"name":"decimals","desc":"Cantidad de decimales a mostrar (por defecto 1)"}],"output":"String con número formateado en notación compacta (ej: '1.2K', '1.5M', '1.0G')"}


id: generate_theme_css_ts_core
name: generate_theme_css
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: generateThemeCss(colors: Record<string, string>, selector?: string): string
tags: ["theme","css","generator","pure"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/generate_theme_css.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 2ee45b40afb287393a3dd79dc1643582dedbcf41c33fb06a33ffa79f4a7419e1
description:
  Genera un bloque CSS con variables de tema a partir de un objeto de tokens. Convierte claves camelCase a kebab-case automaticamente. Pura — solo transforma datos, no accede al DOM.

example:
  ```typescript
  import { generateThemeCss } from './generate_theme_css'
  import { themeConfigToColors } from './theme_config_to_colors'
  import { darkTheme } from '../ui/themes'

  // Generar CSS para inyectar en <style>
  const colors = themeConfigToColors(darkTheme)
  const css = generateThemeCss(colors)
  // Output:
  // :root {
  //   --background: oklch(8% 0.015 260);
  //   --foreground: oklch(95% 0.01 260);
  //   --card: oklch(12% 0.015 260);
  //   --card-foreground: oklch(95% 0.01 260);
  //   ...
  // }

  // Inyectar en el documento
  const style = document.createElement('style')
  style.textContent = css
  document.head.appendChild(style)

  // Generar para selector especifico (dark mode)
  const darkCss = generateThemeCss(colors, '.dark')
  // Output: .dark { --background: oklch(...); ... }

  // Generar para multiples selectores
  const lightCss = generateThemeCss(lightColors, ':root')
  const darkCss2 = generateThemeCss(darkColors, ':root.dark')
  const combined = [lightCss, darkCss2].join('\n\n')
  ```

notes:
  Funcion pura — sin acceso al DOM, sin side effects. Util para SSR, generacion de archivos CSS estaticos, o pre-generar temas en build time.

  La conversion camelCase → kebab-case es simple (reemplaza mayusculas con `-` + minuscula). No maneja casos especiales como `backgroundColor` → `background-color`; los tokens del registry ya usan nombres semanticos directos (`background`, `cardForeground`, etc.).

  Compone naturalmente con `themeConfigToColors` del registry: `generateThemeCss(themeConfigToColors(config))`.

code:
  /**
   * Genera un bloque CSS con variables de tema para inyectar como &lt;style&gt; o
   * escribir en un archivo .css.
   *
   * Convierte claves camelCase a kebab-case automaticamente:
   *   `cardForeground` → `--card-foreground`
   *
   * @param colors - Objeto con tokens de tema. Claves en camelCase, valores CSS.
   * @param selector - Selector CSS donde aplicar las variables. Por defecto `:root`.
   * @returns String CSS con el bloque completo.
   */
  export function generateThemeCss(
    colors: Record<string, string>,
    selector: string = ':root',
  ): string {
    const lines = Object.entries(colors)
      .map(([key, value]) => {
        const cssName = key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
        return `  --${cssName}: ${value};`
      })
      .join('\n')
    return `${selector} {\n${lines}\n}`
  }

params_schema:
  {"params":[{"name":"colors","desc":"Objeto con pares clave-valor de nombre variable CSS a valor de color"},{"name":"selector","desc":"Selector CSS donde inyectar variables (':root' por defecto)"}],"output":"String con bloque CSS completo conteniendo definiciones de variables de tema"}


id: get_computed_color_ts_core
name: get_computed_color
kind: function
lang: ts
domain: core
version: 1.0.0
purity: impure
signature: getComputedColor(cssVar: string): string
tags: ["theme","css","color","runtime","dom","canvas"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: frontend/functions/core/get_computed_color.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 1ff249841ef1967ab44fd952e402c3bb0f6160fa776a20b48d0ce1d44b349b4b
description:
  Resuelve una CSS variable de color a su valor RGB computado por el browser. Acepta '--primary', 'primary' o 'var(--primary)'. Util para canvas, sigma.js y APIs que no soportan CSS variables.

example:
  ```typescript
  import { getComputedColor } from './get_computed_color'

  // Todos estos formatos son equivalentes:
  const color1 = getComputedColor('--primary')      // "rgb(120, 80, 220)"
  const color2 = getComputedColor('primary')        // "rgb(120, 80, 220)"
  const color3 = getComputedColor('var(--primary)') // "rgb(120, 80, 220)"

  // Usar en canvas 2D
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = getComputedColor('--background')
  ctx.strokeStyle = getComputedColor('--border')

  // Usar en sigma.js v2
  renderer.setSetting('defaultNodeColor', getComputedColor('--primary'))
  renderer.setSetting('defaultEdgeColor', getComputedColor('--muted'))
  ```

notes:
  Impura — muta el DOM temporalmente (append + remove) para forzar la resolucion del color. Solo disponible en browser.

  El browser retorna el valor en formato `rgb(r, g, b)` o `rgba(r, g, b, a)`, no en el formato original de la CSS variable (oklch, hsl, etc.). Si necesitas el valor original sin resolver, usar `getThemeTokens` en su lugar.

  Para multiples colores en un mismo render, llamar una vez y cachear los resultados — cada llamada hace un ciclo DOM.

code:
  /**
   * Resuelve una CSS variable de color a su valor RGB computado por el browser.
   *
   * Acepta cualquiera de los formatos:
   *   - `"--primary"`
   *   - `"primary"` (sin prefijo --)
   *   - `"var(--primary)"`
   *
   * Retorna el valor en formato `rgb(r, g, b)` o `rgba(r, g, b, a)` segun
   * como lo expanda el browser. Util para canvas, sigma.js y cualquier API
   * que no soporte CSS variables directamente.
   */
  export function getComputedColor(cssVar: string): string {
    const name = cssVar.startsWith('var(')
      ? cssVar.slice(4, -1).trim()
      : cssVar.startsWith('--')
        ? cssVar
        : `--${cssVar}`

    const el = document.createElement('div')
    el.style.color = `var(${name})`
    el.style.display = 'none'
    document.body.appendChild(el)
    const computed = getComputedStyle(el).color
    document.body.removeChild(el)
    return computed
  }

params_schema:
  {"params":[{"name":"cssVar","desc":"Variable CSS a resolver (acepta '--primary', 'primary' o 'var(--primary)')"}],"output":"String con valor RGB computado por el browser (ej: 'rgb(120, 80, 220)')"}


id: get_series_color_ts_core
name: get_series_color
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: getSeriesColor(index: number, color?: string): string
tags: ["chart","color","series","visualization"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/get_series_color.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: e7b89c8dd2fde12d8b7ca66b378660fd159a08adb16b07c5df11edb858452ced
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/chart-base.tsx
description:
  Devuelve color para una serie de gráfico por índice cíclico, o el color explícito si se proporciona.

example:
  ```typescript
  getSeriesColor(0)            // '#3b82f6'
  getSeriesColor(5)            // '#3b82f6' (cicla sobre 5 colores)
  getSeriesColor(0, '#ff0000') // '#ff0000' (usa el explícito)
  ```

notes:
  Paleta fija de 5 colores: azul, verde, ámbar, violeta, rosa. También exporta `defaultColors` para uso directo.

code:
  const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']

  export function getSeriesColor(index: number, color?: string): string {
    return color || defaultColors[index % defaultColors.length]
  }

  export { defaultColors }

params_schema:
  {"params":[{"name":"index","desc":"Índice de serie (ciclará sobre paleta de 5 colores)"},{"name":"color","desc":"Color explícito a usar si se proporciona (opcional)"}],"output":"String con color HEX o el color explícito proporcionado"}


id: get_theme_tokens_ts_core
name: get_theme_tokens
kind: function
lang: ts
domain: core
version: 1.0.0
purity: impure
signature: getThemeTokens(): ThemeTokens
tags: ["theme","css","tokens","runtime","dom"]
uses_functions: []
uses_types: []
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: frontend/functions/core/get_theme_tokens.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: fc1a2cdf47a068af475b694c61dd20582ba37b651e7fd41dc0d2357ebb7b6dfb
description:
  Lee todas las CSS variables de tema del documento y devuelve un objeto tipado con los valores computados desde :root. Util para pasar colores a APIs que no entienden CSS variables (canvas, sigma.js, D3).

example:
  ```typescript
  import { getThemeTokens } from './get_theme_tokens'

  const tokens = getThemeTokens()

  // Pasar colores a sigma.js (que no soporta CSS variables)
  const sigmaSettings = {
    defaultNodeColor: tokens.primary,
    defaultEdgeColor: tokens.muted,
    labelColor: { color: tokens.foreground },
  }

  // Pasar colores a un canvas 2D
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = tokens.background
  ctx.strokeStyle = tokens.border
  ```

notes:
  Impura — accede a `document.documentElement` y `getComputedStyle`. Solo disponible en browser.

  Los valores retornados son los valores sin procesar de las CSS variables (ej: `oklch(8% 0.015 260)`). Para obtener valores RGB computed (necesarios para algunas APIs), usar `getComputedColor`.

  Funciona con cualquier tema activo: el resultado cambia automaticamente cuando se cambia el tema via `applyTheme`.

code:
  /** Tokens de tema leidos de las CSS variables activas en :root. */
  export interface ThemeTokens {
    background: string
    foreground: string
    card: string
    cardForeground: string
    popover: string
    popoverForeground: string
    primary: string
    primaryForeground: string
    secondary: string
    secondaryForeground: string
    muted: string
    mutedForeground: string
    accent: string
    accentForeground: string
    destructive: string
    destructiveForeground: string
    success: string
    successForeground: string
    border: string
    input: string
    ring: string
  }

  /**
   * Lee todas las CSS variables de tema del documento y devuelve un objeto
   * tipado con los valores computados desde :root.
   *
   * Util para pasar colores a APIs que no entienden CSS variables
   * (canvas, sigma.js, D3, etc.).
   */
  export function getThemeTokens(): ThemeTokens {
    const style = getComputedStyle(document.documentElement)
    const get = (name: string) => style.getPropertyValue(`--${name}`).trim()
    return {
      background: get('background'),
      foreground: get('foreground'),
      card: get('card'),
      cardForeground: get('card-foreground'),
      popover: get('popover'),
      popoverForeground: get('popover-foreground'),
      primary: get('primary'),
      primaryForeground: get('primary-foreground'),
      secondary: get('secondary'),
      secondaryForeground: get('secondary-foreground'),
      muted: get('muted'),
      mutedForeground: get('muted-foreground'),
      accent: get('accent'),
      accentForeground: get('accent-foreground'),
      destructive: get('destructive'),
      destructiveForeground: get('destructive-foreground'),
      success: get('success'),
      successForeground: get('success-foreground'),
      border: get('border'),
      input: get('input'),
      ring: get('ring'),
    }
  }

params_schema:
  {"output":"Objeto ThemeTokens con todas las variables CSS de tema resueltas (colores, tipografía, espaciado)"}


id: theme_config_to_colors_ts_core
name: theme_config_to_colors
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
signature: themeConfigToColors(config: ThemeConfig): ThemeColors
tags: ["theme","colors","css-variables","conversion"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/theme_config_to_colors.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 03b0e93c8710902f71e80b70543f9ef06b6cba0efae3ba29a771480b897d756b
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/themes/types.ts
description:
  Convierte un ThemeConfig completo a ThemeColors plano para inyectar como CSS variables. Mapea tokens semánticos a variables CSS.

example:
  ```typescript
  const colors = themeConfigToColors(darkThemeConfig)
  // { background: '...', foreground: '...', primary: '...', ... }
  ```

notes:
  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).

code:
  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,
    }
  }

params_schema:
  {"params":[{"name":"config","desc":"Configuración de tema con propiedades semánticas de color"}],"output":"Objeto ThemeColors con variables CSS estandarizadas mapeadas de la config"}


id: wails_cache_ts_core
name: wails_cache
kind: function
lang: ts
domain: core
version: 1.0.0
purity: pure
tags: ["wails","cache","ipc","reactive","state"]
uses_functions: []
uses_types: []
returns: []
imports: []
tests: []
file_path: frontend/functions/core/wails_cache.ts
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: fcdbc7db041c22eb5b51c3e6ae938454bd0fc18e98e1d8261b4b85b66442996f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/lib/wails/cache.ts
signature:
  class WailsCache { get<T>(key: string[]): T | null; set<T>(key: string[], data: T): void; invalidate(key: string[]): void; subscribe(key: string[], cb: () => void): () => void }

description:
  Cache reactivo para IPC Wails con invalidación por prefijo, suscripción a cambios y tracking de staleness. Singleton global.

example:
  ```typescript
  import { wailsCache } from './wails_cache'

  wailsCache.set(['users', '123'], userData)
  const user = wailsCache.get<User>(['users', '123'])
  wailsCache.invalidate(['users']) // invalida users:*
  const unsub = wailsCache.subscribe(['users'], () => console.log('changed'))
  ```

notes:
  Key como string[] permite invalidación jerárquica: `invalidate(['users'])` invalida `users`, `users:123`, `users:456`, etc.

code:
  interface CacheEntry {
    data: unknown
    timestamp: Date
  }

  export class WailsCache {
    private cache = new Map<string, CacheEntry>()
    private subscribers = new Map<string, Set<() => void>>()

    /** Generar key string desde array */
    private getKey(queryKey: string[]): string {
      return queryKey.join(':')
    }

    /** Obtener dato del cache */
    get<T>(queryKey: string[]): T | null {
      const entry = this.cache.get(this.getKey(queryKey))
      return (entry?.data as T) ?? null
    }

    /** Guardar dato en cache */
    set<T>(queryKey: string[], data: T): void {
      const key = this.getKey(queryKey)
      this.cache.set(key, { data, timestamp: new Date() })
      this.notifySubscribers(key)
    }

    /** Verificar si existe en cache */
    has(queryKey: string[]): boolean {
      return this.cache.has(this.getKey(queryKey))
    }

    /** Obtener timestamp de última actualización */
    getTimestamp(queryKey: string[]): Date | null {
      const entry = this.cache.get(this.getKey(queryKey))
      return entry?.timestamp ?? null
    }

    /** Verificar si los datos están stale */
    isStale(queryKey: string[], staleTime: number): boolean {
      const entry = this.cache.get(this.getKey(queryKey))
      if (!entry) return true
      return Date.now() - entry.timestamp.getTime() > staleTime
    }

    /** Invalidar cache (esta key y todas las que empiezan igual) */
    invalidate(queryKey: string[]): void {
      const prefix = this.getKey(queryKey)
      const keysToDelete: string[] = []

      for (const key of this.cache.keys()) {
        if (key === prefix || key.startsWith(prefix + ':')) {
          keysToDelete.push(key)
        }
      }

      keysToDelete.forEach((key) => {
        this.cache.delete(key)
        this.notifySubscribers(key)
      })
    }

    /** Limpiar todo el cache */
    clear(): void {
      this.cache.clear()
      this.subscribers.forEach((_, key) => this.notifySubscribers(key))
    }

    /** Subscribirse a cambios en una key */
    subscribe(queryKey: string[], callback: () => void): () => void {
      const key = this.getKey(queryKey)
      if (!this.subscribers.has(key)) {
        this.subscribers.set(key, new Set())
      }
      this.subscribers.get(key)!.add(callback)

      return () => {
        this.subscribers.get(key)?.delete(callback)
      }
    }

    /** Notificar a subscribers */
    private notifySubscribers(key: string): void {
      this.subscribers.get(key)?.forEach((callback) => callback())
    }

    /** Obtener tamaño del cache */
    get size(): number {
      return this.cache.size
    }

    /** Obtener todas las keys */
    keys(): string[] {
      return Array.from(this.cache.keys())
    }
  }

  // Singleton global
  export const wailsCache = new WailsCache()

params_schema:
  {"output":"Clase WailsCache con métodos get, set, invalidate, subscribe para gestionar caché reactiva con invalidación jerárquica"}


id: accordion_ts_ui
name: accordion
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Accordion(props: AccordionProps): JSX.Element
tags: ["accordion","collapsible","component","ui","interactive","base-ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["@base-ui/react/collapsible","lucide-react"]
tests: []
file_path: frontend/functions/ui/accordion.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: []
content_hash: 64143c4ab8e1651a6cf610ee9b3438b6972575d9268a82e08574ae50fcd27d33
description:
  Secciones colapsables con animaciones. Base-UI Collapsible primitive. Composable: AccordionItem + AccordionTrigger + AccordionContent.

example:
  ```tsx
  <Accordion>
    <AccordionItem defaultOpen>
      <AccordionTrigger>Seccion 1</AccordionTrigger>
      <AccordionContent>
        Contenido de la primera seccion.
      </AccordionContent>
    </AccordionItem>
    <AccordionItem>
      <AccordionTrigger>Seccion 2</AccordionTrigger>
      <AccordionContent>
        Contenido de la segunda seccion.
      </AccordionContent>
    </AccordionItem>
  </Accordion>
  ```

props:
  [{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales para el contenedor"}]

notes:
  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.

code:
  import * as React from "react"
  import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
  import { ChevronDownIcon } from "lucide-react"
  import { cn } from "../core/cn"

  interface AccordionItem {
    value: string
    trigger: React.ReactNode
    content: React.ReactNode
    disabled?: boolean
  }

  interface AccordionProps {
    items?: AccordionItem[]
    type?: "single" | "multiple"
    defaultValue?: string | string[]
    className?: string
    itemClassName?: string
    children?: React.ReactNode
  }

  function Accordion({ className, children, ...props }: React.ComponentProps<"div"> & AccordionProps) {
    return (
      <div data-slot="accordion" className={cn("divide-y divide-border", className)} {...props}>
        {children}
      </div>
    )
  }

  interface AccordionItemProps extends CollapsiblePrimitive.Root.Props {
    className?: string
  }

  function AccordionItem({ className, ...props }: AccordionItemProps) {
    return (
      <CollapsiblePrimitive.Root
        data-slot="accordion-item"
        className={cn("group/accordion-item", className)}
        {...props}
      />
    )
  }

  function AccordionTrigger({ className, children, ...props }: CollapsiblePrimitive.Trigger.Props) {
    return (
      <CollapsiblePrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          "flex w-full items-center justify-between py-4 text-sm font-medium transition-all outline-none",
          "hover:underline focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:underline",
          "disabled:pointer-events-none disabled:opacity-50",
          "[&[data-open]>svg]:rotate-180",
          className
        )}
        {...props}
      >
        {children}
        <ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
      </CollapsiblePrimitive.Trigger>
    )
  }

  function AccordionContent({ className, children, ...props }: CollapsiblePrimitive.Panel.Props) {
    return (
      <CollapsiblePrimitive.Panel
        data-slot="accordion-content"
        className={cn(
          "overflow-hidden text-sm",
          "data-open:animate-in data-open:fade-in-0",
          "data-closed:animate-out data-closed:fade-out-0",
          className
        )}
        {...props}
      >
        <div className="pb-4">{children}</div>
      </CollapsiblePrimitive.Panel>
    )
  }

  export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
  export type { AccordionItem as AccordionItemData, AccordionProps }

params_schema:
  {"output":"Componente Accordion que renderiza secciones colapsables con soporte para múltiples items abiertos simultáneamente"}


id: alert_ts_ui
name: alert
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Alert(props: { variant?: 'default' | 'destructive' }): JSX.Element
tags: ["alert","feedback","component","ui","notification"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react","class-variance-authority"]
tests: []
file_path: frontend/functions/ui/alert.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["default","destructive"]
content_hash: 74dd89804c4449d10411ed62d5a1118d88f33d1cf50ce78f55f1bb444e572637
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/alert.tsx
description:
  Alerta accesible con variantes default y destructive. Sistema de slots para título, descripción, icono y acción.

example:
  ```tsx
  <Alert variant="destructive">
    <AlertCircle />
    <AlertTitle>Error</AlertTitle>
    <AlertDescription>Something went wrong.</AlertDescription>
  </Alert>
  ```

props:
  [{"name":"variant","type":"'default' | 'destructive'","required":false,"description":"Variante visual"}]

notes:
  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).

code:
  import * as React from "react"
  import { cva, type VariantProps } from "class-variance-authority"
  import { cn } from "../core/cn"

  const alertVariants = cva(
    "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
    {
      variants: {
        variant: {
          default: "bg-card text-card-foreground",
          destructive: "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
        },
      },
      defaultVariants: { variant: "default" },
    }
  )

  function Alert({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
    return <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
  }

  function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
    return <div data-slot="alert-title" className={cn("font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", className)} {...props} />
  }

  function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
    return <div data-slot="alert-description" className={cn("text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4", className)} {...props} />
  }

  function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
    return <div data-slot="alert-action" className={cn("absolute top-2 right-2", className)} {...props} />
  }

  export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants }

params_schema:
  {"output":"Componente Alert que renderiza una alerta accesible con slots para título, descripción, icono y acción"}


id: analytics_page_ts_ui
name: analytics_page
kind: function
lang: ts
domain: ui
version: 1.0.0
purity: pure
signature: analyticsPage(props: AnalyticsPageProps): ReactElement
tags: ["analytics","dashboard","kpi","charts","factory","composition","ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/analytics_page.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: ddc4b3c9fff3e72c3a7c7fd202dd27536dd98d72659efa27f2c98123ccb15d3e
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
description:
  Genera un dashboard de analytics completo con header, fila de KPIs con deltas y grid de charts configurables.

example:
  ```tsx
  analyticsPage({
    title: 'Sales Analytics',
    metrics: [
      { label: 'Revenue', value: '$124,500', delta: { value: 12.5, isPositive: true } },
      { label: 'Orders', value: '1,234', delta: { value: -3.2, isPositive: false } },
      { label: 'Avg Order', value: '$101', delta: { value: 0, isPositive: true } },
      { label: 'Customers', value: '892' },
    ],
    charts: [
      { id: 'revenue', title: 'Revenue Over Time', type: 'area', span: 2, content: <AreaChart data={revenueData} xKey="month" yKey="revenue" /> },
      { id: 'orders', title: 'Orders by Category', type: 'bar', content: <BarChart data={orderData} xKey="category" yKey="count" /> },
      { id: 'trends', title: 'Customer Trends', type: 'line', content: <LineChart data={trendData} xKey="week" yKey="customers" /> },
    ],
  })
  ```

notes:
  Layout inteligente: los KPIs se ajustan automáticamente a 2/3/4 columnas según cantidad. Los charts soportan span para ancho completo.

code:
  import * as React from 'react'
  import { cn } from '../core/cn'

  interface MetricConfig {
    label: string
    value: string | number
    delta?: { value: number; isPositive: boolean }
    sparklineData?: number[]
  }

  interface ChartConfig {
    id: string
    title: string
    type: 'line' | 'bar' | 'area'
    span?: 1 | 2
    height?: number
    content: React.ReactNode
  }

  interface AnalyticsPageProps {
    title: string
    subtitle?: string
    dateRange?: React.ReactNode
    metrics: MetricConfig[]
    charts: ChartConfig[]
    actions?: React.ReactNode
    className?: string
  }

  export function analyticsPage({
    title,
    subtitle,
    dateRange,
    metrics,
    charts,
    actions,
    className,
  }: AnalyticsPageProps): React.ReactElement {
    return (
      <div className={cn('space-y-6', className)}>
        {/* Header */}
        <div className="flex items-center justify-between border-b pb-4">
          <div className="space-y-1">
            <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
            {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
          </div>
          <div className="flex items-center gap-2">
            {dateRange}
            {actions}
          </div>
        </div>

        {/* KPI Row */}
        <div className={cn(
          'grid gap-4',
          metrics.length <= 2 ? 'grid-cols-1 md:grid-cols-2' :
          metrics.length <= 3 ? 'grid-cols-1 md:grid-cols-3' :
          'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
        )}>
          {metrics.map((metric, i) => (
            <div key={i} className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
              <p className="text-sm text-muted-foreground">{metric.label}</p>
              <div className="mt-2 flex items-end justify-between gap-4">
                <div className="space-y-1">
                  <p className="text-3xl font-bold tracking-tight">{metric.value}</p>
                  {metric.delta && (
                    <div className={cn(
                      'flex items-center gap-1 text-sm font-medium',
                      metric.delta.value === 0 ? 'text-muted-foreground' :
                      metric.delta.isPositive ? 'text-green-600 dark:text-green-500' :
                      'text-red-600 dark:text-red-500'
                    )}>
                      <span>{metric.delta.value > 0 ? '+' : ''}{metric.delta.value}%</span>
                    </div>
                  )}
                </div>
              </div>
            </div>
          ))}
        </div>

        {/* Charts Grid */}
        <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
          {charts.map((chart) => (
            <div
              key={chart.id}
              className={cn(
                'rounded-xl border bg-card p-4 text-card-foreground shadow-sm',
                chart.span === 2 && 'lg:col-span-2'
              )}
            >
              <h3 className="mb-3 text-sm font-medium text-muted-foreground">{chart.title}</h3>
              {chart.content}
            </div>
          ))}
        </div>
      </div>
    )
  }

  export type { AnalyticsPageProps, MetricConfig, ChartConfig }

params_schema:
  {"params":[{"name":"props","desc":"Configuración del dashboard: título, métricas con deltas, y lista de charts con span"}],"output":"Componente ReactElement que renderiza un dashboard de analytics completo con header, KPIs y grid de charts"}


id: apply_theme_ts_ui
name: apply_theme
kind: function
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: applyTheme(theme: Theme): void
tags: ["theme","css-variables","apply","runtime","ui"]
uses_functions: []
uses_types: ["ThemeConfig_ts_ui"]
returns: []
error_type: error_go_core
imports: []
tests: []
file_path: frontend/functions/ui/apply_theme.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 74dd962e43d072bf8592509c1684dddfd39386702e5248cf099dc4400040436f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/hooks/use-theme.tsx
description:
  Inyecta un tema como CSS variables en document.documentElement. Maneja clase dark automáticamente. Mapea 40 tokens semánticos.

example:
  ```typescript
  import { applyTheme } from './apply_theme'

  applyTheme({
    name: 'dark',
    label: 'Oscuro',
    colors: themeConfigToColors(darkThemeConfig)
  })
  ```

notes:
  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.

code:
  interface ThemeColors {
    background: string
    foreground: string
    card: string
    cardForeground: string
    popover: string
    popoverForeground: string
    primary: string
    primaryForeground: string
    secondary: string
    secondaryForeground: string
    muted: string
    mutedForeground: string
    accent: string
    accentForeground: string
    destructive: string
    destructiveForeground: string
    success: string
    successForeground: string
    warning: string
    warningForeground: string
    info: string
    infoForeground: string
    surface: string
    surfaceHover: string
    overlay: string
    border: string
    input: string
    ring: string
    chart1: string
    chart2: string
    chart3: string
    chart4: string
    chart5: string
    sidebar: string
    sidebarForeground: string
    sidebarPrimary: string
    sidebarPrimaryForeground: string
    sidebarAccent: string
    sidebarAccentForeground: string
    sidebarBorder: string
    sidebarRing: string
  }

  interface Theme {
    name: string
    label: string
    colors: ThemeColors
  }

  const cssVarMap: Record<keyof ThemeColors, string> = {
    background: '--background',
    foreground: '--foreground',
    card: '--card',
    cardForeground: '--card-foreground',
    popover: '--popover',
    popoverForeground: '--popover-foreground',
    primary: '--primary',
    primaryForeground: '--primary-foreground',
    secondary: '--secondary',
    secondaryForeground: '--secondary-foreground',
    muted: '--muted',
    mutedForeground: '--muted-foreground',
    accent: '--accent',
    accentForeground: '--accent-foreground',
    destructive: '--destructive',
    destructiveForeground: '--destructive-foreground',
    success: '--success',
    successForeground: '--success-foreground',
    warning: '--warning',
    warningForeground: '--warning-foreground',
    info: '--info',
    infoForeground: '--info-foreground',
    surface: '--surface',
    surfaceHover: '--surface-hover',
    overlay: '--overlay',
    border: '--border',
    input: '--input',
    ring: '--ring',
    chart1: '--chart-1',
    chart2: '--chart-2',
    chart3: '--chart-3',
    chart4: '--chart-4',
    chart5: '--chart-5',
    sidebar: '--sidebar',
    sidebarForeground: '--sidebar-foreground',
    sidebarPrimary: '--sidebar-primary',
    sidebarPrimaryForeground: '--sidebar-primary-foreground',
    sidebarAccent: '--sidebar-accent',
    sidebarAccentForeground: '--sidebar-accent-foreground',
    sidebarBorder: '--sidebar-border',
    sidebarRing: '--sidebar-ring',
  }

  export function applyTheme(theme: Theme): void {
    const root = document.documentElement
    const colors = theme.colors

    Object.entries(cssVarMap).forEach(([key, cssVar]) => {
      const value = colors[key as keyof ThemeColors]
      root.style.setProperty(cssVar, value)
    })

    if (theme.name === 'dark' || theme.name === 'midnight' || theme.name === 'sunset') {
      root.classList.add('dark')
    } else {
      root.classList.remove('dark')
    }
  }

  export type { Theme, ThemeColors }

params_schema:
  {"params":[{"name":"theme","desc":"Objeto Theme con nombre, label y colores a inyectar como CSS variables"}],"output":"Void - función impura que inyecta CSS variables en document.documentElement"}


id: area_chart_ts_ui
name: area_chart
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: AreaChart(props: AreaChartProps): JSX.Element
description: Gráfico de área Recharts con gradientes automáticos, multi-series, stacking y tooltips temáticos.
tags: ["chart","area","visualization","recharts","gradient","component","ui"]
uses_functions: ["cn_ts_core","chart_container_ts_ui","get_series_color_ts_core"]
uses_types: ["ChartSeries_ts_ui"]
returns: []
imports: ["recharts"]
tests: []
file_path: frontend/functions/ui/area_chart.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["default","stacked"]
content_hash: fe9168d87d5902711eedf58de502425d1cd34fc16ec77d52368fbcb6ed569c8a
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/area-chart.tsx
example:
  ```tsx
  <AreaChart data={data} xKey="date" yKey="revenue" gradient />
  <AreaChart data={data} xKey="date" series={series} stacked showLegend />
  ```

props:
  [{"name":"data","type":"Record\u003cstring, unknown\u003e[]","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"}]

code:
  import {
    AreaChart as RechartsAreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
  } from 'recharts'
  import { ChartContainer, ChartTooltipContent, type Series, getSeriesColor } from './chart_container'

  interface GradientConfig { from: string; to: string }

  interface AreaChartProps {
    data: Record<string, unknown>[]
    xKey: string
    yKey?: string
    series?: Series[]
    stacked?: boolean
    gradient?: GradientConfig | boolean
    showGrid?: boolean
    showLegend?: boolean
    height?: number | string
    className?: string
    xAxisFormatter?: (value: unknown) => string
    yAxisFormatter?: (value: unknown) => string
    valueFormatter?: (value: number) => string
  }

  function AreaChartComponent({
    data, xKey, yKey, series, stacked = false, gradient = true, showGrid = true,
    showLegend = false, height = 300, className, xAxisFormatter, yAxisFormatter,
    valueFormatter = (v) => v.toLocaleString(),
  }: AreaChartProps) {
    const areas = series
      ? series.map((s, i) => ({ dataKey: s.key, name: s.name, color: getSeriesColor(i, s.color) }))
      : yKey ? [{ dataKey: yKey, name: yKey, color: getSeriesColor(0) }] : []

    const gradientConfig: GradientConfig | null = gradient
      ? typeof gradient === 'object' ? gradient : { from: '', to: 'transparent' }
      : null

    return (
      <ChartContainer className={className} height={height}>
        <RechartsAreaChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
          <defs>
            {areas.map((area) => (
              <linearGradient key={area.dataKey} id={`gradient-${area.dataKey}`} x1="0" y1="0" x2="0" y2="1">
                <stop offset="5%" stopColor={gradientConfig?.from || area.color} stopOpacity={0.8} />
                <stop offset="95%" stopColor={gradientConfig?.to || area.color} stopOpacity={0.1} />
              </linearGradient>
            ))}
          </defs>
          {showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />}
          <XAxis dataKey={xKey} tickFormatter={xAxisFormatter} className="text-xs fill-muted-foreground" />
          <YAxis tickFormatter={yAxisFormatter} className="text-xs fill-muted-foreground" />
          <Tooltip content={<ChartTooltipContent valueFormatter={valueFormatter} />} cursor={{ stroke: 'hsl(var(--muted-foreground))', strokeDasharray: '3 3' }} />
          {showLegend && <Legend />}
          {areas.map((area) => (
            <Area key={area.dataKey} type="monotone" dataKey={area.dataKey} name={area.name} stroke={area.color} strokeWidth={2} fill={gradient ? `url(#gradient-${area.dataKey})` : area.color} fillOpacity={gradient ? 1 : 0.3} stackId={stacked ? 'stack' : undefined} />
          ))}
        </RechartsAreaChart>
      </ChartContainer>
    )
  }

  export const AreaChart = AreaChartComponent
  export type { AreaChartProps, GradientConfig }

params_schema:
  {"output":"Componente JSX que renderiza un gráfico de área con gradientes, multi-series y tooltips temáticos"}


id: avatar_ts_ui
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: []
imports: ["class-variance-authority"]
tests: []
file_path: frontend/functions/ui/avatar.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["xs","sm","md","lg","xl"]
content_hash: 6d95e0fdb3e15fdcf306c428b1f89bb0d021d1cae2592969791cf963f5f5cdab
example:
  ```tsx
  // Con imagen
  <Avatar src="https://example.com/user.jpg" alt="Juan Perez" size="md" />

  // Con fallback a iniciales
  <Avatar fallback="Juan Perez" size="lg" />

  // Iniciales explicitas
  <Avatar initials="JD" size="sm" />

  // Maneja error de imagen automaticamente
  <Avatar src="/broken-url.jpg" fallback="Maria Garcia" />
  ```

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' -\u003e '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"}]

notes:
  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.

code:
  import * as React from "react"
  import { cva, type VariantProps } from "class-variance-authority"
  import { cn } from "../core/cn"

  const avatarVariants = cva(
    "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground select-none",
    {
      variants: {
        size: {
          xs: "size-6 text-xs",
          sm: "size-8 text-sm",
          md: "size-10 text-base",
          lg: "size-12 text-lg",
          xl: "size-16 text-xl",
        },
      },
      defaultVariants: { size: "md" },
    }
  )

  interface AvatarProps
    extends React.ComponentPropsWithoutRef<"span">,
      VariantProps<typeof avatarVariants> {
    src?: string
    alt?: string
    fallback?: string
    initials?: string
  }

  function getInitials(name?: string): string {
    if (!name) return "?"
    const parts = name.trim().split(/\s+/)
    if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
    return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
  }

  const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
    ({ className, size, src, alt, fallback, initials, ...props }, ref) => {
      const [imgError, setImgError] = React.useState(false)
      const showImage = src && !imgError
      const displayInitials = initials ?? getInitials(fallback ?? alt)

      return (
        <span
          ref={ref}
          data-slot="avatar"
          className={cn(avatarVariants({ size }), className)}
          {...props}
        >
          {showImage ? (
            <img
              src={src}
              alt={alt ?? ""}
              className="aspect-square size-full object-cover"
              onError={() => setImgError(true)}
            />
          ) : (
            <span data-slot="avatar-fallback" aria-hidden="true">
              {displayInitials}
            </span>
          )}
        </span>
      )
    }
  )
  Avatar.displayName = "Avatar"

  export { Avatar, avatarVariants }
  export type { AvatarProps }

params_schema:
  {"output":"Componente Avatar que renderiza imagen de usuario circular con fallback a iniciales generadas"}


id: badge_ts_ui
name: badge
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Badge(props: BadgeProps & VariantProps<typeof badgeVariants>): JSX.Element
tags: ["badge","status","component","ui","indicator"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["class-variance-authority"]
example:
  ```tsx
  <Badge variant="success">Active</Badge>
  <Badge variant="error" size="sm">Error</Badge>
  ```

tests: []
file_path: frontend/functions/ui/badge.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["default","secondary","destructive","outline","ghost","link","success","warning","error","info"]
content_hash: b000611b92422320307044b34b8d1f108d35e87666f49c164b8b6ea48b80b387
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/badge.tsx
description:
  Badge con 10 variantes semánticas (default, secondary, destructive, outline, ghost, link, success, warning, error, info) y 2 tamaños.

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"}]

notes:
  Versión simplificada que usa span nativo en lugar de useRender de Base-UI. Mantiene todas las variantes y la composibilidad con cn().

code:
  import * as React from "react"
  import { cva, type VariantProps } from "class-variance-authority"
  import { cn } from "../core/cn"

  const badgeVariants = cva(
    "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:pointer-events-none [&>svg]:size-3!",
    {
      variants: {
        variant: {
          default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
          secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
          destructive: "bg-destructive/10 text-destructive [a]:hover:bg-destructive/20",
          outline: "border-border text-foreground [a]:hover:bg-muted",
          ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
          link: "text-primary underline-offset-4 hover:underline",
          success: "bg-green-500/10 text-green-600 dark:bg-green-500/20 dark:text-green-400",
          warning: "bg-yellow-500/10 text-yellow-600 dark:bg-yellow-500/20 dark:text-yellow-400",
          error: "bg-red-500/10 text-red-600 dark:bg-red-500/20 dark:text-red-400",
          info: "bg-blue-500/10 text-blue-600 dark:bg-blue-500/20 dark:text-blue-400",
        },
        size: {
          default: "h-5 px-2 text-xs",
          sm: "h-4 px-1.5 text-[10px]",
        },
      },
      defaultVariants: {
        variant: "default",
        size: "default",
      },
    }
  )

  interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}

  function Badge({ className, variant = "default", size = "default", ...props }: BadgeProps) {
    return (
      <span
        data-slot="badge"
        className={cn(badgeVariants({ variant, size }), className)}
        {...props}
      />
    )
  }

  export { Badge, badgeVariants }

params_schema:
  {"output":"Componente Badge que renderiza un indicador visual con 10 variantes semánticas de estado"}


id: bar_chart_ts_ui
name: bar_chart
kind: component
lang: ts
domain: ui
version: 1.1.0
purity: impure
signature: BarChart(props: BarChartProps): JSX.Element
tags: ["chart","bar","visualization","recharts","component","ui"]
uses_functions: ["cn_ts_core","chart_container_ts_ui","get_series_color_ts_core"]
uses_types: ["ChartSeries_ts_ui"]
returns: []
imports: ["recharts"]
tests: []
file_path: frontend/functions/ui/bar_chart.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["vertical","horizontal"]
content_hash: cab1fc2876d7e03fc997ded35123e382212a1199a31c8bff3b4802482388a755
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/bar-chart.tsx
description:
  Gráfico de barras Recharts con multi-series, orientación horizontal/vertical, tooltips temáticos y bordes redondeados.

example:
  ```tsx
  <BarChart data={data} xKey="category" yKey="sales" showLegend />
  <BarChart data={data} xKey="name" series={series} horizontal />
  ```

props:
  [{"name":"data","type":"Record\u003cstring, unknown\u003e[]","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"}]

notes:
  En modo `horizontal=true`: el layout de Recharts es `'vertical'`, YAxis recibe `dataKey={xKey}` con `type="category"` (categorías en eje Y), XAxis recibe `type="number"` (valores en eje X). El radius de las barras se ajusta a `[0, 4, 4, 0]` para redondear la punta derecha. Este intercambio de ejes es obligatorio — sin él las barras horizontales no se renderizan.

code:
  import {
    BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
  } from 'recharts'
  import { ChartContainer, ChartTooltipContent, type Series, getSeriesColor } from './chart_container'

  interface BarChartProps {
    data: Record<string, unknown>[]
    xKey: string
    yKey?: string
    series?: Series[]
    horizontal?: boolean
    showGrid?: boolean
    showLegend?: boolean
    height?: number | string
    className?: string
    xAxisFormatter?: (value: unknown) => string
    yAxisFormatter?: (value: unknown) => string
    valueFormatter?: (value: number) => string
  }

  function BarChartComponent({
    data, xKey, yKey, series, horizontal = false, showGrid = true, showLegend = false,
    height = 300, className, xAxisFormatter, yAxisFormatter, valueFormatter = (v) => v.toLocaleString(),
  }: BarChartProps) {
    const bars = series
      ? series.map((s, i) => ({ dataKey: s.key, name: s.name, fill: getSeriesColor(i, s.color) }))
      : yKey ? [{ dataKey: yKey, name: yKey, fill: getSeriesColor(0) }] : []

    return (
      <ChartContainer className={className} height={height}>
        <RechartsBarChart data={data} layout={horizontal ? 'vertical' : 'horizontal'} margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
          {showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />}
          {horizontal ? (
            <>
              <XAxis type="number" tickFormatter={yAxisFormatter} className="text-xs fill-muted-foreground" />
              <YAxis dataKey={xKey} type="category" tickFormatter={xAxisFormatter} width={80} className="text-xs fill-muted-foreground" />
            </>
          ) : (
            <>
              <XAxis dataKey={xKey} tickFormatter={xAxisFormatter} className="text-xs fill-muted-foreground" />
              <YAxis tickFormatter={yAxisFormatter} className="text-xs fill-muted-foreground" />
            </>
          )}
          <Tooltip content={<ChartTooltipContent valueFormatter={valueFormatter} />} cursor={{ fill: 'hsl(var(--muted) / 0.3)' }} />
          {showLegend && <Legend />}
          {bars.map((bar) => <Bar key={bar.dataKey} dataKey={bar.dataKey} name={bar.name} fill={bar.fill} radius={horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0]} />)}
        </RechartsBarChart>
      </ChartContainer>
    )
  }

  export const BarChart = BarChartComponent
  export type { BarChartProps }

params_schema:
  {"output":"Componente JSX que renderiza un gráfico de barras vertical u horizontal con multi-series y tooltips temáticos"}


id: breadcrumb_ts_ui
name: breadcrumb
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Breadcrumb(props: BreadcrumbProps): JSX.Element
tags: ["breadcrumb","navigation","component","ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["lucide-react"]
tests: []
file_path: frontend/functions/ui/breadcrumb.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: [{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]
emits: []
framework: react
variant: []
content_hash: e555ae721e427da2d027a19b4b150fb522b61abaa54994682b71ba712c87d2ee
description:
  Navegacion jerarquica con separadores, elipsis para paths largos y soporte para router links via asChild.

example:
  ```tsx
  <Breadcrumb>
    <BreadcrumbList>
      <BreadcrumbItem>
        <BreadcrumbLink href="/">Inicio</BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbSeparator />
      <BreadcrumbItem>
        <BreadcrumbLink href="/docs">Documentacion</BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbSeparator />
      <BreadcrumbItem>
        <BreadcrumbPage>Componentes</BreadcrumbPage>
      </BreadcrumbItem>
    </BreadcrumbList>
  </Breadcrumb>

  // Con elipsis para paths largos
  <Breadcrumb>
    <BreadcrumbList>
      <BreadcrumbItem>
        <BreadcrumbLink href="/">Inicio</BreadcrumbLink>
      </BreadcrumbItem>
      <BreadcrumbSeparator />
      <BreadcrumbItem>
        <BreadcrumbEllipsis />
      </BreadcrumbItem>
      <BreadcrumbSeparator />
      <BreadcrumbItem>
        <BreadcrumbPage>Pagina actual</BreadcrumbPage>
      </BreadcrumbItem>
    </BreadcrumbList>
  </Breadcrumb>
  ```

notes:
  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.

code:
  import * as React from "react"
  import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
  import { cn } from "../core/cn"

  function Breadcrumb({ ...props }: React.ComponentPropsWithoutRef<"nav">) {
    return <nav data-slot="breadcrumb" aria-label="breadcrumb" {...props} />
  }

  function BreadcrumbList({ className, ...props }: React.ComponentPropsWithoutRef<"ol">) {
    return (
      <ol
        data-slot="breadcrumb-list"
        className={cn("flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", className)}
        {...props}
      />
    )
  }

  function BreadcrumbItem({ className, ...props }: React.ComponentPropsWithoutRef<"li">) {
    return (
      <li
        data-slot="breadcrumb-item"
        className={cn("inline-flex items-center gap-1.5", className)}
        {...props}
      />
    )
  }

  function BreadcrumbLink({
    className,
    href,
    asChild,
    children,
    ...props
  }: React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }) {
    if (asChild) {
      return (
        <span data-slot="breadcrumb-link" className={cn("transition-colors hover:text-foreground", className)} {...(props as React.ComponentPropsWithoutRef<"span">)}>
          {children}
        </span>
      )
    }
    return (
      <a
        data-slot="breadcrumb-link"
        href={href}
        className={cn("transition-colors hover:text-foreground", className)}
        {...props}
      >
        {children}
      </a>
    )
  }

  function BreadcrumbPage({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
    return (
      <span
        data-slot="breadcrumb-page"
        role="link"
        aria-current="page"
        aria-disabled="true"
        className={cn("font-medium text-foreground", className)}
        {...props}
      />
    )
  }

  function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
    return (
      <li
        data-slot="breadcrumb-separator"
        role="presentation"
        aria-hidden="true"
        className={cn("[&>svg]:size-3.5", className)}
        {...props}
      >
        {children ?? <ChevronRightIcon />}
      </li>
    )
  }

  function BreadcrumbEllipsis({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
    return (
      <span
        data-slot="breadcrumb-ellipsis"
        role="presentation"
        aria-hidden="true"
        className={cn("flex size-9 items-center justify-center", className)}
        {...props}
      >
        <MoreHorizontalIcon className="size-4" />
        <span className="sr-only">More</span>
      </span>
    )
  }

  export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator }

params_schema:
  {"output":"Componente Breadcrumb que renderiza navegación jerárquica con separadores, elipsis y soporte para router links"}


id: button_ts_ui
name: button
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Button(props: ButtonProps & VariantProps<typeof buttonVariants>): JSX.Element
tags: ["button","component","ui","interactive","cva"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["@base-ui/react","class-variance-authority"]
tests: []
file_path: frontend/functions/ui/button.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:44:15Z
emits: ["onClick"]
framework: react
variant: ["default","outline","secondary","ghost","destructive","link"]
content_hash: 8533c58640068ae48f5deeff81881825ae7df18935e4c14228d0a7cb15f12771
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/button.tsx
params_schema: {"output":"JSX.Element: botón renderizado con los estilos y comportamientos configurados"}
description:
  Botón accesible con 6 variantes (default, outline, secondary, ghost, destructive, link) y 8 tamaños. Base-UI primitivo con CVA.

example:
  ```tsx
  <Button variant="outline" size="sm">Click me</Button>
  <Button variant="destructive">Delete</Button>
  <Button variant="ghost" size="icon"><TrashIcon /></Button>
  ```

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"}]

notes:
  Componente base del sistema. Usa Base-UI Button primitive para accesibilidad completa (keyboard, ARIA). CVA para gestión type-safe de variantes.

code:
  "use client"

  import { Button as ButtonPrimitive } from "@base-ui/react/button"
  import { cva, type VariantProps } from "class-variance-authority"
  import { cn } from "../core/cn"

  const buttonVariants = cva(
    "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
    {
      variants: {
        variant: {
          default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
          outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
          secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
          ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
          destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
          link: "text-primary underline-offset-4 hover:underline",
        },
        size: {
          default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
          xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs",
          sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem]",
          lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
          icon: "size-8",
          "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)]",
          "icon-sm": "size-7 rounded-[min(var(--radius-md),12px)]",
          "icon-lg": "size-9",
        },
      },
      defaultVariants: {
        variant: "default",
        size: "default",
      },
    }
  )

  function Button({
    className,
    variant = "default",
    size = "default",
    ...props
  }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
    return (
      <ButtonPrimitive
        data-slot="button"
        className={cn(buttonVariants({ variant, size, className }))}
        {...props}
      />
    )
  }

  export { Button, buttonVariants }


id: card_ts_ui
name: card
kind: component
lang: ts
domain: ui
version: 1.1.0
purity: impure
tags: ["card","container","layout","component","ui","dashboard","dark"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/card.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["default","sm","borderless","ghost"]
content_hash: 997589638833dab02e0dbe542e574472ba39b3b8ec61acbfcc9208dea37f5911
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/card.tsx
signature:
  Card(props: { size?: 'default' | 'sm'; variant?: 'default' | 'borderless' | 'ghost'; className?: string; children: ReactNode }): JSX.Element

description:
  Contenedor card con header, title, description, action, content y footer. Sistema de slots composable. Variantes default, borderless y ghost para dashboards dark.

example:
  ```tsx
  <Card>
    <CardHeader>
      <CardTitle>Título</CardTitle>
      <CardDescription>Descripción</CardDescription>
    </CardHeader>
    <CardContent>Contenido</CardContent>
    <CardFooter>Footer</CardFooter>
  </Card>

  {/* Dashboard dark — sin bordes */}
  <Card variant="borderless">
    <CardContent>Widget sin marco</CardContent>
  </Card>

  {/* Completamente transparente */}
  <Card variant="ghost">
    <CardContent>Sin fondo ni borde</CardContent>
  </Card>
  ```

props:
  [{"name":"size","type":"'default' | 'sm'","required":false,"description":"Tamaño del card"},{"name":"variant","type":"'default' | 'borderless' | 'ghost'","required":false,"description":"Variante visual. borderless quita borde/shadow, ghost además hace bg transparente"},{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]

notes:
  Sistema de slots via data-slot attributes. Card detecta automáticamente la presencia de CardFooter y ajusta el padding. Exporta 7 subcomponentes composables.

  Las variantes `borderless` y `ghost` eliminan el `ring-1` del borde por defecto. `ghost` además hace el fondo transparente. Alternativa con CSS global: `[data-slot="card"] { --tw-ring-opacity: 0; }` o `[data-variant="borderless"] { ring: 0 }` via `data-variant` attribute expuesto.

code:
  import * as React from "react"
  import { cn } from "../core/cn"

  type CardVariant = "default" | "borderless" | "ghost"

  function Card({
    className,
    size = "default",
    variant = "default",
    ...props
  }: React.ComponentProps<"div"> & { size?: "default" | "sm"; variant?: CardVariant }) {
    return (
      <div
        data-slot="card"
        data-size={size}
        data-variant={variant}
        className={cn(
          "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
          variant === "default" && "ring-1 ring-foreground/10",
          variant === "borderless" && "ring-0 shadow-none",
          variant === "ghost" && "ring-0 shadow-none bg-transparent",
          className
        )}
        {...props}
      />
    )
  }

  function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
    return (
      <div
        data-slot="card-header"
        className={cn(
          "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
          className
        )}
        {...props}
      />
    )
  }

  function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
    return (
      <div
        data-slot="card-title"
        className={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
        {...props}
      />
    )
  }

  function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
    return (
      <div
        data-slot="card-description"
        className={cn("text-sm text-muted-foreground", className)}
        {...props}
      />
    )
  }

  function CardAction({ className, ...props }: React.ComponentProps<"div">) {
    return (
      <div
        data-slot="card-action"
        className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
        {...props}
      />
    )
  }

  function CardContent({ className, ...props }: React.ComponentProps<"div">) {
    return (
      <div
        data-slot="card-content"
        className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
        {...props}
      />
    )
  }

  function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
    return (
      <div
        data-slot="card-footer"
        className={cn("flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3", className)}
        {...props}
      />
    )
  }

  export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }

params_schema:
  {"output":"Componente Card que renderiza un contenedor con slots composables (header, content, footer) y 3 variantes visuales"}


id: chart_container_ts_ui
name: chart_container
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: ChartContainer(props: { children: ReactNode; height?: number | string }): JSX.Element
tags: ["chart","container","recharts","base","visualization","component","ui"]
uses_functions: ["cn_ts_core","get_series_color_ts_core"]
uses_types: ["ChartSeries_ts_ui"]
returns: []
imports: ["recharts","react"]
tests: []
file_path: frontend/functions/ui/chart_container.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["default"]
content_hash: 8702af0a79b106e9c01825a777b8559505d2dca782c59682b090b7317470c298
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/chart-base.tsx
description:
  Base para todos los charts Recharts: container responsive, tooltip temático, legend y utilidades de colores por serie.

example:
  ```tsx
  <ChartContainer height={400}>
    <RechartsLineChart data={data}>...</RechartsLineChart>
  </ChartContainer>
  ```

props:
  [{"name":"height","type":"number | string","required":false,"description":"Altura del chart (default 300)"},{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]

notes:
  Exporta: ChartContainer, ChartTooltipContent, ChartTooltip, ChartLegend, chartColors, defaultColors, getSeriesColor, Series.

code:
  import * as React from 'react'
  import { cn } from '../core/cn'
  import { ResponsiveContainer, Tooltip as RechartsTooltip, Legend as RechartsLegend } from 'recharts'

  export const chartColors = [
    'hsl(var(--chart-1, 220 70% 50%))',
    'hsl(var(--chart-2, 160 60% 45%))',
    'hsl(var(--chart-3, 30 80% 55%))',
    'hsl(var(--chart-4, 280 65% 60%))',
    'hsl(var(--chart-5, 340 75% 55%))',
  ]

  export const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']

  export interface Series {
    key: string
    name: string
    color?: string
  }

  export function getSeriesColor(index: number, color?: string): string {
    return color || defaultColors[index % defaultColors.length]
  }

  interface ChartContainerProps {
    children: React.ReactNode
    className?: string
    height?: number | string
  }

  export function ChartContainer({ children, className, height = 300 }: ChartContainerProps) {
    return (
      <div
        className={cn('w-full', className)}
        style={{ height, minWidth: 100, minHeight: typeof height === 'number' ? Math.min(height, 100) : 100 }}
      >
        <ResponsiveContainer width="100%" height="100%">
          {children as React.ReactElement}
        </ResponsiveContainer>
      </div>
    )
  }

  interface ChartTooltipContentProps {
    active?: boolean
    payload?: Array<{ name: string; value: number; color: string; dataKey: string }>
    label?: string
    labelFormatter?: (label: string) => string
    valueFormatter?: (value: number) => string
  }

  export function ChartTooltipContent({
    active, payload, label,
    labelFormatter = (l) => l,
    valueFormatter = (v) => v.toLocaleString(),
  }: ChartTooltipContentProps) {
    if (!active || !payload?.length) return null
    return (
      <div className="rounded-lg border bg-background p-2 shadow-md">
        <p className="mb-1 text-sm font-medium">{labelFormatter(label || '')}</p>
        <div className="space-y-0.5">
          {payload.map((entry, index) => (
            <div key={index} className="flex items-center gap-2 text-sm">
              <div className="size-2.5 rounded-full" style={{ backgroundColor: entry.color }} />
              <span className="text-muted-foreground">{entry.name}:</span>
              <span className="font-medium">{valueFormatter(entry.value)}</span>
            </div>
          ))}
        </div>
      </div>
    )
  }

  export function ChartTooltip(props: React.ComponentProps<typeof RechartsTooltip>) {
    return <RechartsTooltip content={<ChartTooltipContent />} cursor={{ fill: 'hsl(var(--muted) / 0.3)' }} {...props} />
  }

  export function ChartLegend(props: React.ComponentProps<typeof RechartsLegend>) {
    return <RechartsLegend wrapperStyle={{ paddingTop: 16 }} {...props} />
  }

params_schema:
  {"output":"Componente ChartContainer que renderiza base responsive para gráficos Recharts con tooltip y legend temáticos"}


id: checkbox_ts_ui
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: []
imports: ["@base-ui/react/checkbox","class-variance-authority"]
tests: []
file_path: frontend/functions/ui/checkbox.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onCheckedChange"]
framework: react
variant: []
content_hash: a9769a67807300751141376a8dabf29c93d55960ff98c4db341bd578cc8c0f49
example:
  ```tsx
  // Basico
  <Checkbox label="Acepto los terminos" />

  // Controlado
  <Checkbox
    label="Seleccionar todos"
    checked={allSelected}
    indeterminate={someSelected}
    onCheckedChange={setAllSelected}
  />

  // Sin label
  <Checkbox checked={isActive} onCheckedChange={setIsActive} />
  ```

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) =\u003e void","required":false,"description":"Callback cuando cambia el estado"}]

notes:
  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.

code:
  import * as React from "react"
  import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
  import { cn } from "../core/cn"

  interface CheckboxProps extends CheckboxPrimitive.Root.Props {
    label?: string
    indeterminate?: boolean
    className?: string
    labelClassName?: string
  }

  function Checkbox({ className, label, id, indeterminate, ...props }: CheckboxProps) {
    const internalId = React.useId()
    const checkboxId = id ?? internalId

    return (
      <div className="flex items-center gap-2">
        <CheckboxPrimitive.Root
          id={checkboxId}
          data-slot="checkbox"
          className={cn(
            "peer size-4 shrink-0 rounded border border-input bg-transparent transition-colors outline-none",
            "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
            "data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground",
            "data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground",
            "disabled:pointer-events-none disabled:opacity-50",
            className
          )}
          indeterminate={indeterminate}
          {...props}
        >
          <CheckboxPrimitive.Indicator
            data-slot="checkbox-indicator"
            className="flex items-center justify-center text-current"
          >
            {indeterminate ? (
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="3"
                strokeLinecap="round"
                className="size-3"
              >
                <line x1="5" y1="12" x2="19" y2="12" />
              </svg>
            ) : (
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="3"
                strokeLinecap="round"
                strokeLinejoin="round"
                className="size-3"
              >
                <polyline points="20 6 9 17 4 12" />
              </svg>
            )}
          </CheckboxPrimitive.Indicator>
        </CheckboxPrimitive.Root>
        {label && (
          <label
            htmlFor={checkboxId}
            data-slot="checkbox-label"
            className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 cursor-pointer select-none"
          >
            {label}
          </label>
        )}
      </div>
    )
  }

  export { Checkbox }
  export type { CheckboxProps }

params_schema:
  {"output":"Componente Checkbox que renderiza input booleano accesible con label opcional y estado indeterminate"}


id: command_ts_ui
name: command
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Command(props: CommandProps): JSX.Element
tags: ["command","search","combobox","component","ui","interactive"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["lucide-react"]
tests: []
file_path: frontend/functions/ui/command.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onValueChange"]
framework: react
variant: []
content_hash: a44fe8fd57f558e4a654785f711727e7a100d20934f6e6d6f5f3fdeded3c6ec8
description:
  Combobox de busqueda y seleccion estilo cmdk. Filtra items por query, soporta grupos, iconos y shortcuts. Incluye CommandSearch para uso de una linea.

example:
  ```tsx
  // Uso simple con CommandSearch
  const items = [
    { value: "react", label: "React", group: "Frameworks" },
    { value: "vue", label: "Vue", group: "Frameworks" },
    { value: "typescript", label: "TypeScript", group: "Lenguajes" },
  ]

  <CommandSearch
    items={items}
    placeholder="Buscar tecnologia..."
    onValueChange={(val) => console.log(val)}
  />

  // Composable para mayor control
  <Command>
    <CommandInput placeholder="Buscar..." value={query} onChange={(e) => setQuery(e.target.value)} />
    <CommandList>
      <CommandEmpty>Sin resultados.</CommandEmpty>
      <CommandGroup heading="Sugerencias">
        <CommandItem selected={selected === "1"} onSelect={() => setSelected("1")}>
          Opcion 1
          <CommandShortcut>⌘K</CommandShortcut>
        </CommandItem>
      </CommandGroup>
    </CommandList>
  </Command>
  ```

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) =\u003e 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.)"}]

notes:
  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.

code:
  import * as React from "react"
  import { SearchIcon, XIcon } from "lucide-react"
  import { cn } from "../core/cn"

  interface CommandItem {
    value: string
    label: string
    description?: string
    icon?: React.ReactNode
    disabled?: boolean
    group?: string
  }

  interface CommandProps {
    items: CommandItem[]
    value?: string
    onValueChange?: (value: string) => void
    placeholder?: string
    emptyMessage?: string
    className?: string
    inputClassName?: string
    listClassName?: string
  }

  function Command({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
    return (
      <div
        data-slot="command"
        className={cn("flex h-full w-full flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground", className)}
        {...props}
      />
    )
  }

  function CommandInput({ className, ...props }: React.ComponentPropsWithoutRef<"input">) {
    return (
      <div data-slot="command-input-wrapper" className="flex items-center border-b px-3">
        <SearchIcon className="mr-2 size-4 shrink-0 text-muted-foreground" />
        <input
          data-slot="command-input"
          className={cn(
            "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none",
            "placeholder:text-muted-foreground",
            "disabled:cursor-not-allowed disabled:opacity-50",
            className
          )}
          {...props}
        />
      </div>
    )
  }

  function CommandList({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
    return (
      <div
        data-slot="command-list"
        className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
        {...props}
      />
    )
  }

  function CommandEmpty({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
    return (
      <div
        data-slot="command-empty"
        className={cn("py-6 text-center text-sm text-muted-foreground", className)}
        {...props}
      />
    )
  }

  function CommandGroup({ className, heading, ...props }: React.ComponentPropsWithoutRef<"div"> & { heading?: string }) {
    return (
      <div data-slot="command-group" className={cn("overflow-hidden p-1 text-foreground", className)}>
        {heading && (
          <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">{heading}</div>
        )}
        <div {...props} />
      </div>
    )
  }

  function CommandSeparator({ className, ...props }: React.ComponentPropsWithoutRef<"div">) {
    return (
      <div
        data-slot="command-separator"
        className={cn("-mx-1 h-px bg-border", className)}
        {...props}
      />
    )
  }

  interface CommandItemProps extends React.ComponentPropsWithoutRef<"div"> {
    selected?: boolean
    disabled?: boolean
    onSelect?: () => void
  }

  function CommandItem({ className, selected, disabled, onSelect, ...props }: CommandItemProps) {
    return (
      <div
        data-slot="command-item"
        data-selected={selected}
        aria-disabled={disabled}
        role="option"
        aria-selected={selected}
        onClick={!disabled ? onSelect : undefined}
        className={cn(
          "relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none",
          "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
          "hover:bg-accent hover:text-accent-foreground",
          disabled && "pointer-events-none opacity-50",
          className
        )}
        {...props}
      />
    )
  }

  function CommandShortcut({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
    return (
      <span
        data-slot="command-shortcut"
        className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
        {...props}
      />
    )
  }

  function CommandSearch({
    items,
    value,
    onValueChange,
    placeholder = "Search...",
    emptyMessage = "No results found.",
    className,
  }: CommandProps) {
    const [query, setQuery] = React.useState("")
    const [selectedValue, setSelectedValue] = React.useState(value ?? "")

    const filtered = React.useMemo(() => {
      if (!query) return items
      const q = query.toLowerCase()
      return items.filter(
        (item) =>
          item.label.toLowerCase().includes(q) ||
          item.description?.toLowerCase().includes(q) ||
          item.value.toLowerCase().includes(q)
      )
    }, [items, query])

    const groups = React.useMemo(() => {
      const map = new Map<string, CommandItem[]>()
      for (const item of filtered) {
        const key = item.group ?? ""
        if (!map.has(key)) map.set(key, [])
        map.get(key)!.push(item)
      }
      return map
    }, [filtered])

    const handleSelect = (val: string) => {
      setSelectedValue(val)
      onValueChange?.(val)
    }

    return (
      <Command className={className}>
        <CommandInput
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder={placeholder}
        />
        <CommandList>
          {filtered.length === 0 ? (
            <CommandEmpty>{emptyMessage}</CommandEmpty>
          ) : (
            Array.from(groups.entries()).map(([group, groupItems]) => (
              <CommandGroup key={group} heading={group || undefined}>
                {groupItems.map((item) => (
                  <CommandItem
                    key={item.value}
                    selected={selectedValue === item.value}
                    disabled={item.disabled}
                    onSelect={() => handleSelect(item.value)}
                  >
                    {item.icon && <span className="shrink-0">{item.icon}</span>}
                    <span>{item.label}</span>
                    {item.description && (
                      <span className="ml-auto text-xs text-muted-foreground">{item.description}</span>
                    )}
                  </CommandItem>
                ))}
              </CommandGroup>
            ))
          )}
        </CommandList>
      </Command>
    )
  }

  export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut }
  export type { CommandItem, CommandProps }

params_schema:
  {"output":"Componente Command que renderiza combobox de búsqueda y selección con filtrado reactivo, grupos e iconos"}


id: crud_page_ts_ui
name: crud_page
kind: function
lang: ts
domain: ui
version: 1.0.0
purity: pure
signature: crudPage<T>(props: CrudPageProps<T>): ReactElement
tags: ["crud","page","table","form","factory","composition","ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/crud_page.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 3373e8105445f1db4b871c0a9ee2574aa5b1f679f388521950c66fd51f7a7f4d
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
description:
  Genera una página CRUD completa con header, tabla con columnas configurables, botones de acción (add/edit/delete) y schema de formulario.

example:
  ```tsx
  crudPage({
    title: 'Users',
    subtitle: 'Manage system users',
    data: users,
    fields: [
      { key: 'name', label: 'Name', type: 'text', required: true },
      { key: 'email', label: 'Email', type: 'email', required: true },
      { key: 'role', label: 'Role', type: 'select', options: [{ label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }] },
    ],
    columns: [
      { key: 'name', label: 'Name' },
      { key: 'email', label: 'Email' },
      { key: 'role', label: 'Role', render: (v) => <Badge variant={v === 'admin' ? 'default' : 'secondary'}>{v}</Badge> },
    ],
    onAdd: handleAdd,
    onEdit: handleEdit,
    onDelete: handleDelete,
  })
  ```

notes:
  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.

code:
  import * as React from 'react'
  import { cn } from '../core/cn'

  interface CrudField {
    key: string
    label: string
    type: 'text' | 'number' | 'email' | 'select' | 'textarea'
    required?: boolean
    options?: Array<{ label: string; value: string }>
    placeholder?: string
  }

  interface CrudPageProps<T extends Record<string, unknown>> {
    title: string
    subtitle?: string
    data: T[]
    fields: CrudField[]
    columns: Array<{
      key: keyof T
      label: string
      render?: (value: unknown, row: T) => React.ReactNode
    }>
    onAdd?: (item: Partial<T>) => void
    onEdit?: (item: T) => void
    onDelete?: (item: T) => void
    actions?: React.ReactNode
    className?: string
  }

  export function crudPage<T extends Record<string, unknown>>({
    title,
    subtitle,
    data,
    fields,
    columns,
    onAdd,
    onEdit,
    onDelete,
    actions,
    className,
  }: CrudPageProps<T>): React.ReactElement {
    return (
      <div className={cn('space-y-6', className)}>
        {/* Header */}
        <div className="flex items-center justify-between border-b pb-4">
          <div className="space-y-1">
            <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
            {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
          </div>
          <div className="flex items-center gap-2">
            {actions}
            {onAdd && (
              <button className="inline-flex h-8 items-center gap-1.5 rounded-lg bg-primary px-2.5 text-sm font-medium text-primary-foreground hover:bg-primary/80">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 5v14M5 12h14"/></svg>
                Add {title.replace(/s$/, '')}
              </button>
            )}
          </div>
        </div>

        {/* Table */}
        <div className="rounded-lg border">
          <table className="w-full caption-bottom text-sm">
            <thead className="border-b bg-muted/50">
              <tr>
                {columns.map((col) => (
                  <th key={String(col.key)} className="px-4 py-3 text-left text-sm font-medium text-muted-foreground">
                    {col.label}
                  </th>
                ))}
                {(onEdit || onDelete) && (
                  <th className="px-4 py-3 text-right text-sm font-medium text-muted-foreground">Actions</th>
                )}
              </tr>
            </thead>
            <tbody className="divide-y">
              {data.length === 0 ? (
                <tr>
                  <td colSpan={columns.length + (onEdit || onDelete ? 1 : 0)} className="h-24 text-center text-muted-foreground">
                    No items yet.
                  </td>
                </tr>
              ) : (
                data.map((row, i) => (
                  <tr key={i} className="hover:bg-muted/50">
                    {columns.map((col) => (
                      <td key={String(col.key)} className="px-4 py-3 align-middle">
                        {col.render ? col.render(row[col.key], row) : String(row[col.key] ?? '')}
                      </td>
                    ))}
                    {(onEdit || onDelete) && (
                      <td className="px-4 py-3 text-right">
                        <div className="flex justify-end gap-1">
                          {onEdit && (
                            <button onClick={() => onEdit(row)} className="inline-flex size-7 items-center justify-center rounded-md hover:bg-muted">
                              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
                            </button>
                          )}
                          {onDelete && (
                            <button onClick={() => onDelete(row)} className="inline-flex size-7 items-center justify-center rounded-md text-destructive hover:bg-destructive/10">
                              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
                            </button>
                          )}
                        </div>
                      </td>
                    )}
                  </tr>
                ))
              )}
            </tbody>
          </table>
        </div>

        {/* Form fields definition (for agent use — renders a form preview) */}
        <div className="hidden" data-slot="crud-form-schema" data-fields={JSON.stringify(fields)} />
      </div>
    )
  }

  export type { CrudPageProps, CrudField }

params_schema:
  {"params":[{"name":"props","desc":"Configuración CRUD: título, datos, columnas de tabla, campos de formulario y callbacks para add/edit/delete"}],"output":"Componente ReactElement que renderiza página CRUD completa con tabla y botones de acción"}


id: dashboard_layout_ts_ui
name: dashboard_layout
kind: function
lang: ts
domain: ui
version: 1.0.0
purity: pure
signature: dashboardLayout(props: DashboardLayoutProps): ReactElement
tags: ["dashboard","layout","grid","factory","composition","ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/dashboard_layout.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: f5760562bb89ed3620a59f73a8aa0458fec0a7517ff2e382acdbad0e6dac10d8
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
description:
  Genera un grid responsive de dashboard a partir de un array de widgets con span configurable. 1-4 columnas con auto-responsive.

example:
  ```tsx
  dashboardLayout({
    columns: 4,
    widgets: [
      { id: 'revenue', title: 'Revenue', content: <KPICard label="Revenue" value="$12k" /> },
      { id: 'users', title: 'Users', content: <KPICard label="Users" value={1234} /> },
      { id: 'chart', title: 'Trends', span: 2, content: <LineChart data={data} xKey="month" yKey="value" /> },
      { id: 'table', span: 4, content: <DataTable columns={cols} data={rows} /> },
    ]
  })
  ```

notes:
  Factory pura — dado el mismo input siempre genera el mismo JSX. Un agente puede construir dashboards completos pasando widgets como configuración declarativa.

code:
  import * as React from 'react'
  import { cn } from '../core/cn'

  interface DashboardWidget {
    id: string
    title?: string
    span?: 1 | 2 | 3 | 4
    rowSpan?: 1 | 2
    content: React.ReactNode
  }

  interface DashboardLayoutProps {
    widgets: DashboardWidget[]
    columns?: 1 | 2 | 3 | 4
    gap?: 'sm' | 'md' | 'lg'
    className?: string
  }

  const gapClasses = { sm: 'gap-2', md: 'gap-4', lg: 'gap-6' }

  const spanClasses: Record<number, string> = {
    1: 'col-span-1',
    2: 'col-span-1 md:col-span-2',
    3: 'col-span-1 md:col-span-2 lg:col-span-3',
    4: 'col-span-1 md:col-span-2 lg:col-span-4',
  }

  const rowSpanClasses: Record<number, string> = {
    1: 'row-span-1',
    2: 'row-span-2',
  }

  export function dashboardLayout({
    widgets,
    columns = 4,
    gap = 'md',
    className,
  }: DashboardLayoutProps): React.ReactElement {
    const gridCols: Record<number, string> = {
      1: 'grid-cols-1',
      2: 'grid-cols-1 md:grid-cols-2',
      3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
      4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
    }

    return (
      <div className={cn('grid', gridCols[columns], gapClasses[gap], className)}>
        {widgets.map((widget) => (
          <div
            key={widget.id}
            className={cn(
              'rounded-xl border bg-card p-4 text-card-foreground shadow-sm',
              spanClasses[widget.span || 1],
              rowSpanClasses[widget.rowSpan || 1]
            )}
          >
            {widget.title && (
              <h3 className="mb-3 text-sm font-medium text-muted-foreground">{widget.title}</h3>
            )}
            {widget.content}
          </div>
        ))}
      </div>
    )
  }

  export type { DashboardWidget, DashboardLayoutProps }

params_schema:
  {"params":[{"name":"props","desc":"Configuración de layout: número de columnas y array de widgets con id, título, contenido y span"}],"output":"Componente ReactElement que renderiza grid responsive de dashboard con ancho adaptable por widget"}


id: data_table_ts_ui
name: data_table
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: DataTable(props: DataTableProps): JSX.Element
tags: ["table","data","heatmap","dashboard","component","ui","format","visualization"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/data_table.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["default","heatmap"]
content_hash: 5ce5acd9dbdeb00d9c4b0c7998a163fded570efe5c4c89e8fa086d69755e45e9
description:
  Tabla de datos con sticky header, overflow scroll, heatmap por columna, formato condicional (number/datetime/currency) y hover rows. Auto-detecta columnas desde la primera fila si no se proveen.

example:
  ```tsx
  // Tabla simple con auto-detección de columnas
  <DataTable data={rows} />

  // Con columnas definidas y heatmap
  <DataTable
    data={metrics}
    columns={[
      { key: 'domain', label: 'Domain' },
      { key: 'count', label: 'Functions', format: ',' },
      { key: 'pure_pct', label: 'Pure %', format: '.1f' },
    ]}
    heatmapColumns={['count', 'pure_pct']}
  />

  // Con formato moneda y fecha
  <DataTable
    data={transactions}
    columns={[
      { key: 'date', label: 'Date', format: 'datetime' },
      { key: 'amount', label: 'Amount', format: '$,.2f', align: 'right' },
      { key: 'description', label: 'Description' },
    ]}
  />
  ```

props:
  [{"name":"data","type":"Record\u003cstring, unknown\u003e[]","required":true,"description":"Filas de datos. Cada objeto es una fila."},{"name":"columns","type":"ColumnDef[]","required":false,"description":"Definición de columnas con key, label, format y align. Si se omite, se auto-detectan desde la primera fila."},{"name":"heatmapColumns","type":"string[]","required":false,"description":"Keys de columnas numéricas que deben colorearse por intensidad (azul oscuro=bajo, azul claro=alto)."},{"name":"maxHeight","type":"number | string","required":false,"description":"Altura máxima antes de scroll. Default 500px."},{"name":"loading","type":"boolean","required":false,"description":"Estado de carga. Muestra spinner si data está vacía."},{"name":"error","type":"Error | null","required":false,"description":"Error a mostrar si la carga falló."},{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales."}]

notes:
  Extraido y generalizado desde `apps/rapid_dashboards/frontend/src/components/widgets/TableWidget.tsx`. El heatmap usa `useMemo` para calcular min/max por columna solo cuando cambian `data` o `heatmapColumns`. La alineación de celdas numéricas es automática (derecha) cuando el valor es `typeof 'number'`; se puede sobreescribir con el campo `align` en ColumnDef.

documentation:
  ## Formatos soportados (campo `format` en ColumnDef)

  | format | Ejemplo input | Output |
  |--------|--------------|--------|
  | `','` | `1234567` | `1,234,567` |
  | `',.2f'` | `1234.5` | `1,234.50` |
  | `'$,.2f'` | `1234.5` | `$1,234.50` |
  | `'.0f'` | `42.7` | `43` |
  | `'datetime'` | `'2026-04-01T12:00:00Z'` | `4/1/2026, 12:00:00 PM` |

code:
  import * as React from 'react'
  import { cn } from '../core/cn'

  interface ColumnDef {
    key: string
    label: string
    /** Format string: ',.2f' | '$,.2f' | 'datetime' | ',' */
    format?: string
    /** Alignment override. Numbers default to right, strings to left. */
    align?: 'left' | 'right' | 'center'
  }

  interface DataTableProps {
    data: Record<string, unknown>[]
    columns?: ColumnDef[]
    /** Column keys that should be colored by value intensity (heatmap). */
    heatmapColumns?: string[]
    maxHeight?: number | string
    className?: string
    loading?: boolean
    error?: Error | null
  }

  function formatCell(value: unknown, format?: string): string {
    if (value == null) return '—'
    if (!format) return String(value)

    if (format === 'datetime' && !isNaN(Date.parse(String(value)))) {
      return new Date(String(value)).toLocaleString()
    }

    const num = Number(value)
    if (!isNaN(num)) {
      if (format.includes('f')) {
        const match = format.match(/\.(\d+)f/)
        const d = match ? parseInt(match[1]) : 0
        let str = num.toFixed(d)
        if (format.includes(',')) {
          str = Number(str).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d })
        }
        if (format.startsWith('$')) str = '$' + str
        return str
      }
      if (format === ',') return num.toLocaleString()
    }
    return String(value)
  }

  function DataTableComponent({
    data,
    columns,
    heatmapColumns = [],
    maxHeight = 500,
    className,
    loading = false,
    error = null,
  }: DataTableProps) {
    // Auto-detect columns from first row if not provided
    const effectiveColumns: ColumnDef[] = (columns && columns.length > 0)
      ? columns
      : (data && data.length > 0)
        ? Object.keys(data[0]).map(k => ({ key: k, label: k }))
        : []

    // Compute heatmap ranges per column
    const heatmapRanges = React.useMemo(() => {
      const ranges: Record<string, { min: number; max: number }> = {}
      if (heatmapColumns.length > 0 && data && data.length > 0) {
        for (const key of heatmapColumns) {
          const values = data.map(r => Number(r[key])).filter(n => !isNaN(n))
          if (values.length > 0) {
            ranges[key] = { min: Math.min(...values), max: Math.max(...values) }
          }
        }
      }
      return ranges
    }, [data, heatmapColumns])

    function heatmapStyle(key: string, value: unknown): React.CSSProperties | undefined {
      const range = heatmapRanges[key]
      if (!range || range.max === range.min) return undefined
      const num = Number(value)
      if (isNaN(num)) return undefined
      const t = (num - range.min) / (range.max - range.min)
      // Dark blue (low) → bright blue (high)
      const alpha = 0.1 + t * 0.55
      return { backgroundColor: `rgba(59, 130, 246, ${alpha})` }
    }

    const maxHeightStyle = typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight

    if (loading && (!data || data.length === 0)) {
      return (
        <div className={cn('flex items-center justify-center text-muted-foreground text-sm', className)}
          style={{ height: 200 }}>
          Loading...
        </div>
      )
    }

    if (error) {
      return (
        <div className={cn('flex items-center justify-center text-destructive text-sm', className)}
          style={{ height: 200 }}>
          {error.message}
        </div>
      )
    }

    return (
      <div className={cn('overflow-auto', className)} style={{ maxHeight: maxHeightStyle }}>
        <table className="w-full text-sm">
          <thead className="sticky top-0 bg-card z-10">
            <tr className="border-b border-border">
              {effectiveColumns.map(col => (
                <th
                  key={col.key}
                  className="text-left py-1.5 px-3 text-xs font-medium text-muted-foreground uppercase tracking-wider whitespace-nowrap"
                >
                  {col.label}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {(data ?? []).map((row, i) => (
              <tr key={i} className="border-b border-border hover:bg-accent/50 transition-colors">
                {effectiveColumns.map(col => {
                  const align = col.align ?? (typeof row[col.key] === 'number' ? 'right' : 'left')
                  return (
                    <td
                      key={col.key}
                      className={cn(
                        'py-1.5 px-3 font-mono text-xs',
                        align === 'right' && 'text-right',
                        align === 'center' && 'text-center',
                      )}
                      style={heatmapStyle(col.key, row[col.key])}
                    >
                      {formatCell(row[col.key], col.format)}
                    </td>
                  )
                })}
              </tr>
            ))}
          </tbody>
        </table>
        {(!data || data.length === 0) && (
          <p className="text-center text-muted-foreground text-sm py-8">No data</p>
        )}
      </div>
    )
  }

  export const DataTable = DataTableComponent
  export type { DataTableProps, ColumnDef }

params_schema:
  {"output":"Componente DataTable que renderiza tabla con sticky header, heatmap condicional y formato automático de datos"}


id: detail_page_ts_ui
name: detail_page
kind: function
lang: ts
domain: ui
version: 1.0.0
purity: pure
signature: detailPage(props: DetailPageProps): ReactElement
tags: ["detail","page","entity","timeline","factory","composition","ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/detail_page.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 7d65fa2d26e1f87bdd2c6d93b715180ad0923aa271083ce7bba47ea73e0344ea
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
description:
  Genera una página de detalle de entidad con header (avatar, badge, back), grid de campos, tabs con contadores y timeline de actividad.

example:
  ```tsx
  detailPage({
    title: 'John Doe',
    subtitle: 'john@example.com',
    badge: <Badge variant="success">Active</Badge>,
    onBack: () => router.back(),
    fields: [
      { label: 'Role', value: 'Administrator' },
      { label: 'Created', value: 'Mar 15, 2026' },
      { label: 'Bio', value: 'Full stack developer...', span: 2 },
    ],
    tabs: [
      { label: 'Projects', value: 'projects', count: 12, content: <ProjectList /> },
      { label: 'Activity', value: 'activity', count: 48, content: <ActivityList /> },
    ],
    activeTab: 'projects',
    timeline: [
      { id: '1', title: 'Deployed v2.1', timestamp: '2 hours ago', variant: 'success' },
      { id: '2', title: 'Updated settings', timestamp: 'Yesterday' },
      { id: '3', title: 'Created project', timestamp: 'Mar 10, 2026' },
    ],
  })
  ```

notes:
  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.

code:
  import * as React from 'react'
  import { cn } from '../core/cn'

  interface DetailField {
    label: string
    value: React.ReactNode
    span?: 1 | 2
  }

  interface DetailTab {
    label: string
    value: string
    content: React.ReactNode
    count?: number
  }

  interface TimelineEvent {
    id: string
    title: string
    description?: string
    timestamp: string
    icon?: React.ReactNode
    variant?: 'default' | 'success' | 'warning' | 'error'
  }

  interface DetailPageProps {
    title: string
    subtitle?: string
    badge?: React.ReactNode
    avatar?: React.ReactNode
    actions?: React.ReactNode
    onBack?: () => void
    fields: DetailField[]
    tabs?: DetailTab[]
    activeTab?: string
    onTabChange?: (value: string) => void
    timeline?: TimelineEvent[]
    className?: string
  }

  const variantDotColors = {
    default: 'bg-primary',
    success: 'bg-green-500',
    warning: 'bg-amber-500',
    error: 'bg-red-500',
  }

  export function detailPage({
    title, subtitle, badge, avatar, actions, onBack,
    fields, tabs, activeTab, onTabChange, timeline, className,
  }: DetailPageProps): React.ReactElement {
    return (
      <div className={cn('space-y-6', className)}>
        {/* Header */}
        <div className="flex items-start justify-between border-b pb-4">
          <div className="flex items-start gap-4">
            {onBack && (
              <button onClick={onBack} className="mt-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-muted">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m15 18-6-6 6-6"/></svg>
              </button>
            )}
            {avatar && <div className="size-12 shrink-0 overflow-hidden rounded-full bg-muted">{avatar}</div>}
            <div className="space-y-1">
              <div className="flex items-center gap-3">
                <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
                {badge}
              </div>
              {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
            </div>
          </div>
          {actions && <div className="flex items-center gap-2">{actions}</div>}
        </div>

        {/* Fields grid */}
        <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
          {fields.map((field, i) => (
            <div key={i} className={cn('space-y-1', field.span === 2 && 'md:col-span-2')}>
              <p className="text-sm text-muted-foreground">{field.label}</p>
              <div className="text-sm font-medium">{field.value}</div>
            </div>
          ))}
        </div>

        {/* Tabs */}
        {tabs && tabs.length > 0 && (
          <div className="space-y-4">
            <nav className="flex gap-4 border-b">
              {tabs.map((tab) => (
                <button
                  key={tab.value}
                  type="button"
                  onClick={() => onTabChange?.(tab.value)}
                  className={cn(
                    'inline-flex items-center gap-2 border-b-2 px-1 pb-3 text-sm font-medium transition-colors',
                    activeTab === tab.value ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'
                  )}
                >
                  {tab.label}
                  {tab.count !== undefined && (
                    <span className="inline-flex h-5 items-center rounded-full bg-muted px-2 text-xs font-medium">{tab.count}</span>
                  )}
                </button>
              ))}
            </nav>
            {tabs.find(t => t.value === activeTab)?.content}
          </div>
        )}

        {/* Timeline */}
        {timeline && timeline.length > 0 && (
          <div className="space-y-3">
            <h3 className="text-sm font-medium text-muted-foreground">Activity</h3>
            <div className="space-y-0">
              {timeline.map((event, i) => (
                <div key={event.id} className="flex gap-3 pb-4">
                  <div className="flex flex-col items-center">
                    <div className={cn('mt-1 size-2.5 rounded-full', variantDotColors[event.variant || 'default'])} />
                    {i < timeline.length - 1 && <div className="flex-1 w-px bg-border" />}
                  </div>
                  <div className="flex-1 space-y-0.5 pb-2">
                    <p className="text-sm font-medium">{event.title}</p>
                    {event.description && <p className="text-xs text-muted-foreground">{event.description}</p>}
                    <p className="text-xs text-muted-foreground/70">{event.timestamp}</p>
                  </div>
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    )
  }

  export type { DetailPageProps, DetailField, DetailTab, TimelineEvent }

params_schema:
  {"params":[{"name":"props","desc":"Configuración de página de detalle: título, avatar, badge, tabs, timeline y campos de metadata"}],"output":"Componente ReactElement que renderiza página de detalle con header, grid de campos y timeline de actividad"}


id: dialog_ts_ui
name: dialog
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Dialog(props: DialogRootProps): JSX.Element
tags: ["dialog","modal","overlay","component","ui","interactive"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["@base-ui/react","lucide-react","react"]
tests: []
file_path: frontend/functions/ui/dialog.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onOpenChange"]
framework: react
variant: ["default"]
content_hash: 1cf9026e3e629d8089d501fea96008cd1c359e63c5746007710842342854b284
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/dialog.tsx
description:
  Diálogo modal accesible con overlay blur, animaciones, close button y sistema de slots (header, footer, title, description).

example:
  ```tsx
  <Dialog>
    <DialogTrigger asChild><Button>Open</Button></DialogTrigger>
    <DialogContent>
      <DialogHeader>
        <DialogTitle>Título</DialogTitle>
        <DialogDescription>Descripción</DialogDescription>
      </DialogHeader>
      <p>Contenido</p>
      <DialogFooter>
        <Button>Confirmar</Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
  ```

props:
  [{"name":"showCloseButton","type":"boolean","required":false,"description":"Mostrar botón de cerrar (default true)"}]

notes:
  10 subcomponentes exportados. Base-UI Dialog primitive para accesibilidad completa (focus trap, escape, click outside).

code:
  import * as React from "react"
  import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
  import { cn } from "../core/cn"
  import { XIcon } from "lucide-react"

  function Dialog({ ...props }: DialogPrimitive.Root.Props) {
    return <DialogPrimitive.Root data-slot="dialog" {...props} />
  }

  function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
    return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
  }

  function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
    return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
  }

  function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
    return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
  }

  function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
    return (
      <DialogPrimitive.Backdrop
        data-slot="dialog-overlay"
        className={cn("fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0", className)}
        {...props}
      />
    )
  }

  function DialogContent({ className, children, showCloseButton = true, ...props }: DialogPrimitive.Popup.Props & { showCloseButton?: boolean }) {
    return (
      <DialogPortal>
        <DialogOverlay />
        <DialogPrimitive.Popup
          data-slot="dialog-content"
          className={cn("fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-sm ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className)}
          {...props}
        >
          {children}
          {showCloseButton && (
            <DialogPrimitive.Close data-slot="dialog-close" className="absolute top-2 right-2 inline-flex size-7 items-center justify-center rounded-md hover:bg-muted">
              <XIcon className="size-4" />
              <span className="sr-only">Close</span>
            </DialogPrimitive.Close>
          )}
        </DialogPrimitive.Popup>
      </DialogPortal>
    )
  }

  function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
    return <div data-slot="dialog-header" className={cn("flex flex-col gap-2", className)} {...props} />
  }

  function DialogFooter({ className, children, ...props }: React.ComponentProps<"div">) {
    return (
      <div data-slot="dialog-footer" className={cn("-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end", className)} {...props}>
        {children}
      </div>
    )
  }

  function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
    return <DialogPrimitive.Title data-slot="dialog-title" className={cn("text-base leading-none font-medium", className)} {...props} />
  }

  function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
    return <DialogPrimitive.Description data-slot="dialog-description" className={cn("text-sm text-muted-foreground", className)} {...props} />
  }

  export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger }

params_schema:
  {"output":"Componente Dialog que renderiza modal accesible con overlay blur, focus trap y sistema de slots composables"}


id: dropdown_menu_ts_ui
name: dropdown_menu
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: DropdownMenu(props: DropdownMenuProps): JSX.Element
tags: ["dropdown","menu","component","ui","interactive","overlay","base-ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["@base-ui/react/menu","lucide-react"]
tests: []
file_path: frontend/functions/ui/dropdown_menu.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onOpenChange"]
framework: react
variant: []
content_hash: 5babfadeb665f420297d2501cddeff462908405132bbef4c4d69c62a72650411
description:
  Menu de acciones y contexto accesible con items, checkboxes, radios, separadores y submenus. Base-UI Menu primitive.

example:
  ```tsx
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button variant="outline">Acciones</Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent>
      <DropdownMenuLabel>Mi cuenta</DropdownMenuLabel>
      <DropdownMenuSeparator />
      <DropdownMenuItem onActivate={() => console.log("Perfil")}>
        Perfil
      </DropdownMenuItem>
      <DropdownMenuCheckboxItem checked={showBookmarks} onCheckedChange={setShowBookmarks}>
        Marcadores
      </DropdownMenuCheckboxItem>
      <DropdownMenuSeparator />
      <DropdownMenuSub>
        <DropdownMenuSubTrigger>Mas opciones</DropdownMenuSubTrigger>
        <DropdownMenuSubContent>
          <DropdownMenuItem>Opcion A</DropdownMenuItem>
        </DropdownMenuSubContent>
      </DropdownMenuSub>
    </DropdownMenuContent>
  </DropdownMenu>
  ```

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) =\u003e void","required":false,"description":"Callback cuando cambia el estado"},{"name":"modal","type":"boolean","required":false,"description":"Comportamiento modal (default: true)"}]

notes:
  Exports: DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuPortal.

code:
  import * as React from "react"
  import { Menu as MenuPrimitive } from "@base-ui/react/menu"
  import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
  import { cn } from "../core/cn"

  function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
    return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
  }

  function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {
    return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />
  }

  function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {
    return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
  }

  function DropdownMenuContent({ className, sideOffset = 4, ...props }: MenuPrimitive.Positioner.Props) {
    return (
      <DropdownMenuPortal>
        <MenuPrimitive.Positioner
          data-slot="dropdown-menu-content"
          sideOffset={sideOffset}
          className="z-50"
          {...props}
        >
          <MenuPrimitive.Popup
            className={cn(
              "min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md",
              "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
              "data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
              "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
              className
            )}
          >
            {props.children}
          </MenuPrimitive.Popup>
        </MenuPrimitive.Positioner>
      </DropdownMenuPortal>
    )
  }

  function DropdownMenuItem({ className, inset, ...props }: MenuPrimitive.Item.Props & { inset?: boolean }) {
    return (
      <MenuPrimitive.Item
        data-slot="dropdown-menu-item"
        className={cn(
          "relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors",
          "focus:bg-accent focus:text-accent-foreground",
          "data-disabled:pointer-events-none data-disabled:opacity-50",
          "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
          inset && "pl-8",
          className
        )}
        {...props}
      />
    )
  }

  function DropdownMenuCheckboxItem({ className, children, checked, ...props }: MenuPrimitive.CheckboxItem.Props) {
    return (
      <MenuPrimitive.CheckboxItem
        data-slot="dropdown-menu-checkbox-item"
        className={cn(
          "relative flex cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none transition-colors",
          "focus:bg-accent focus:text-accent-foreground",
          "data-disabled:pointer-events-none data-disabled:opacity-50",
          className
        )}
        checked={checked}
        {...props}
      >
        <span className="absolute left-2 flex size-4 items-center justify-center">
          <MenuPrimitive.CheckboxItemIndicator>
            <CheckIcon className="size-4" />
          </MenuPrimitive.CheckboxItemIndicator>
        </span>
        {children}
      </MenuPrimitive.CheckboxItem>
    )
  }

  function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
    return <MenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />
  }

  function DropdownMenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) {
    return (
      <MenuPrimitive.RadioItem
        data-slot="dropdown-menu-radio-item"
        className={cn(
          "relative flex cursor-default select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none transition-colors",
          "focus:bg-accent focus:text-accent-foreground",
          "data-disabled:pointer-events-none data-disabled:opacity-50",
          className
        )}
        {...props}
      >
        <span className="absolute left-2 flex size-4 items-center justify-center">
          <MenuPrimitive.RadioItemIndicator>
            <CircleIcon className="size-2 fill-current" />
          </MenuPrimitive.RadioItemIndicator>
        </span>
        {children}
      </MenuPrimitive.RadioItem>
    )
  }

  function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {
    return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
  }

  function DropdownMenuLabel({ className, inset, ...props }: MenuPrimitive.GroupLabel.Props & { inset?: boolean }) {
    return (
      <MenuPrimitive.GroupLabel
        data-slot="dropdown-menu-label"
        className={cn("px-2 py-1.5 text-xs font-medium text-muted-foreground", inset && "pl-8", className)}
        {...props}
      />
    )
  }

  function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<"div">) {
    return (
      <div
        data-slot="dropdown-menu-separator"
        className={cn("-mx-1 my-1 h-px bg-border", className)}
        {...props}
      />
    )
  }

  function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
    return (
      <span
        data-slot="dropdown-menu-shortcut"
        className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
        {...props}
      />
    )
  }

  function DropdownMenuSub({ ...props }: MenuPrimitive.Root.Props) {
    return <MenuPrimitive.Root data-slot="dropdown-menu-sub" {...props} />
  }

  function DropdownMenuSubTrigger({ className, inset, children, ...props }: MenuPrimitive.SubmenuTrigger.Props & { inset?: boolean }) {
    return (
      <MenuPrimitive.SubmenuTrigger
        data-slot="dropdown-menu-sub-trigger"
        className={cn(
          "flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors",
          "focus:bg-accent focus:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground",
          "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
          inset && "pl-8",
          className
        )}
        {...props}
      >
        {children}
        <ChevronRightIcon className="ml-auto" />
      </MenuPrimitive.SubmenuTrigger>
    )
  }

  function DropdownMenuSubContent({ className, ...props }: MenuPrimitive.Positioner.Props) {
    return (
      <MenuPrimitive.Portal>
        <MenuPrimitive.Positioner data-slot="dropdown-menu-sub-content" className="z-50" {...props}>
          <MenuPrimitive.Popup
            className={cn(
              "min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md",
              "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
              "data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
              className
            )}
          >
            {props.children}
          </MenuPrimitive.Popup>
        </MenuPrimitive.Positioner>
      </MenuPrimitive.Portal>
    )
  }

  export {
    DropdownMenu,
    DropdownMenuCheckboxItem,
    DropdownMenuContent,
    DropdownMenuGroup,
    DropdownMenuItem,
    DropdownMenuLabel,
    DropdownMenuPortal,
    DropdownMenuRadioGroup,
    DropdownMenuRadioItem,
    DropdownMenuSeparator,
    DropdownMenuShortcut,
    DropdownMenuSub,
    DropdownMenuSubContent,
    DropdownMenuSubTrigger,
    DropdownMenuTrigger,
  }

params_schema:
  {"output":"Componente DropdownMenu que renderiza menú desplegable accesible con items, checkboxes, radios y submenus"}


id: form_field_ts_ui
name: form_field
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: FormField(props: FormFieldProps): JSX.Element
tags: ["form","field","label","error","component","ui","accessibility"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/form_field.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["default"]
content_hash: 3299bface945494302766b5412964b4fc473dc7bfae2ba122a9bd6311c7f075c
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/form-field.tsx
description:
  Wrapper de campo de formulario con label, helper text, error y ARIA automáticos. Inyecta id y aria-describedby a hijos.

example:
  ```tsx
  <FormField label="Email" helperText="Tu email corporativo" error={errors.email}>
    <Input type="email" />
  </FormField>
  ```

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"}]

code:
  import * as React from "react"
  import { cn } from "../core/cn"

  interface FormFieldProps {
    label?: string
    helperText?: string
    error?: string
    children: React.ReactNode
    className?: string
  }

  function FormField({ label, helperText, error, children, className }: FormFieldProps) {
    const id = React.useId()
    const inputId = `${id}-input`
    const helperId = `${id}-helper`
    const errorId = `${id}-error`

    const describedBy = [helperText ? helperId : null, error ? errorId : null].filter(Boolean).join(" ") || undefined

    const childWithProps = React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        return React.cloneElement(child as React.ReactElement<Record<string, unknown>>, {
          id: inputId,
          "aria-invalid": error ? true : undefined,
          "aria-describedby": describedBy,
        })
      }
      return child
    })

    return (
      <div className={cn("flex flex-col gap-1.5", className)}>
        {label && <label htmlFor={inputId} className="text-sm font-medium text-foreground">{label}</label>}
        {childWithProps}
        {helperText && !error && <p id={helperId} className="text-sm text-muted-foreground">{helperText}</p>}
        {error && <p id={errorId} className="text-sm text-destructive">{error}</p>}
      </div>
    )
  }

  export { FormField }
  export type { FormFieldProps }

params_schema:
  {"output":"Componente FormField que renderiza wrapper de campo con label, helper text, error y ARIA automáticos"}


id: graph_container_ts_ui
name: graph_container
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: GraphContainer(props: GraphContainerProps): JSX.Element
description: Interactive graph visualization with sigma.js, graphology, and ForceAtlas2 layout
tags: ["component","ui","graph","visualization","sigma","graphology","network"]
uses_functions: []
uses_types: []
returns: []
imports: ["graphology","graphology-layout-forceatlas2","sigma"]
tests: []
file_path: frontend/functions/ui/graph/index.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: []
content_hash: c9265a72805bd748347d5cb654ff0bdc480ee3320186598f916737b42e97aefd
example:
  ```tsx
  import { GraphContainer } from '@fn_library/graph'
  import type { GraphData } from '@fn_library/graph'

  const data: GraphData = {
    nodes: [
      { id: '1', label: 'Node A', color: '#e74c3c', size: 10 },
      { id: '2', label: 'Node B', color: '#3498db', size: 8 },
    ],
    edges: [
      { id: 'e1', source: '1', target: '2', label: 'connects', type: 'arrow' },
    ],
  }

  function MyGraph() {
    return (
      <GraphContainer
        data={data}
        layout="organic"
        showLegend
        nodeTypes={[
          { type: 'person', color: '#e74c3c', label: 'Person' },
          { type: 'org', color: '#3498db', label: 'Organization' },
        ]}
        onNodeClick={(node) => console.log('clicked:', node.id)}
        height="500px"
      />
    )
  }
  ```

props:
  [{"name":"data","type":"GraphData","required":true,"description":"Graph data with nodes and edges arrays"},{"name":"layout","type":"'organic' | 'random'","required":false,"description":"Layout algorithm (default: organic/ForceAtlas2)"},{"name":"showLegend","type":"boolean","required":false,"description":"Show node type legend overlay"},{"name":"nodeTypes","type":"NodeType[]","required":false,"description":"Node type definitions for legend"},{"name":"onNodeClick","type":"(node: GraphNode) =\u003e void","required":false,"description":"Node click handler"},{"name":"onNodeDoubleClick","type":"(node: GraphNode) =\u003e void","required":false,"description":"Node double-click handler"},{"name":"theme","type":"GraphTheme","required":false,"description":"Visual theme overrides"},{"name":"height","type":"string | number","required":false,"description":"Container height (default: 100%)"},{"name":"className","type":"string","required":false,"description":"Additional CSS classes"}]

notes:
  - Usa graphology como estructura de datos de grafo
  - ForceAtlas2 para layout organico (iterations adaptativas segun numero de nodos)
  - Sigma.js para renderizado WebGL de alto rendimiento
  - Soporta grafos dirigidos multi-edge
  - El componente limpia la instancia Sigma al desmontar

code:
  import * as React from "react"
  import Graph from "graphology"
  import forceAtlas2 from "graphology-layout-forceatlas2"
  import { Sigma } from "sigma"

  // ── Types ─────────────────────────────────────────────────────────────────

  export interface GraphNode {
    id: string
    label: string
    type?: string
    color?: string
    size?: number
    x?: number
    y?: number
    [key: string]: unknown
  }

  export interface GraphEdge {
    id: string
    source: string
    target: string
    label?: string
    color?: string
    size?: number
    type?: "arrow" | "line"
    weight?: number
    [key: string]: unknown
  }

  export interface GraphData {
    nodes: GraphNode[]
    edges: GraphEdge[]
  }

  export interface NodeType {
    type: string
    color: string
    label: string
  }

  export interface GraphTheme {
    backgroundColor?: string
    nodeColor?: string
    nodeSize?: number
    edgeColor?: string
    edgeSize?: number
    labelColor?: string
    selectionColor?: string
  }

  export interface GraphContainerProps {
    data: GraphData
    layout?: "organic" | "random"
    showToolbar?: boolean
    showLegend?: boolean
    showMinimap?: boolean
    nodeTypes?: NodeType[]
    onNodeClick?: (node: GraphNode) => void
    onNodeDoubleClick?: (node: GraphNode) => void
    enableSelection?: boolean
    selectionMode?: "single" | "multiple"
    theme?: GraphTheme
    height?: string | number
    className?: string
  }

  const DEFAULT_THEME: Required<GraphTheme> = {
    backgroundColor: "var(--background, #0a0a0f)",
    nodeColor: "#95a5a6",
    nodeSize: 8,
    edgeColor: "rgba(255,255,255,0.19)",
    edgeSize: 1,
    labelColor: "#e0e0e0",
    selectionColor: "#3b82f6",
  }

  // ── Component ─────────────────────────────────────────────────────────────

  function GraphContainer({
    data,
    layout = "organic",
    showLegend = false,
    nodeTypes = [],
    onNodeClick,
    onNodeDoubleClick,
    theme: themeProp,
    height = "100%",
    className,
  }: GraphContainerProps) {
    const containerRef = React.useRef<HTMLDivElement>(null)
    const sigmaRef = React.useRef<Sigma | null>(null)
    const graphRef = React.useRef<Graph | null>(null)
    const theme = React.useMemo(
      () => ({ ...DEFAULT_THEME, ...themeProp }),
      [themeProp],
    )

    // Build + render
    React.useEffect(() => {
      const el = containerRef.current
      if (!el) return

      // Cleanup previous instance
      if (sigmaRef.current) {
        sigmaRef.current.kill()
        sigmaRef.current = null
      }

      const g = new Graph({ multi: true, type: "directed" })
      graphRef.current = g

      // Add nodes
      for (const n of data.nodes) {
        g.addNode(n.id, {
          label: n.label,
          x: n.x ?? (Math.random() - 0.5) * 10,
          y: n.y ?? (Math.random() - 0.5) * 10,
          size: n.size ?? theme.nodeSize,
          color: n.color ?? theme.nodeColor,
          type: n.type,
        })
      }

      // Add edges
      for (const e of data.edges) {
        try {
          g.addEdgeWithKey(e.id, e.source, e.target, {
            label: e.label,
            size: e.size ?? theme.edgeSize,
            color: e.color ?? theme.edgeColor,
            type: e.type === "arrow" ? "arrow" : "line",
            weight: e.weight ?? 1,
          })
        } catch {
          // skip duplicate keys
        }
      }

      // Layout
      if (layout === "organic" && g.order > 0) {
        forceAtlas2.assign(g, {
          iterations: Math.min(500, Math.max(100, g.order * 5)),
          settings: {
            gravity: 1,
            scalingRatio: 2,
            slowDown: 5,
            barnesHutOptimize: g.order > 300,
          },
        })
      }

      // Render
      const renderer = new Sigma(g, el, {
        renderEdgeLabels: false,
        defaultEdgeColor: theme.edgeColor,
        defaultNodeColor: theme.nodeColor,
        labelColor: { color: theme.labelColor },
        labelSize: 11,
      })

      sigmaRef.current = renderer

      // Events
      if (onNodeClick) {
        renderer.on("clickNode", ({ node }) => {
          const attrs = g.getNodeAttributes(node)
          onNodeClick({ id: node, ...attrs } as unknown as GraphNode)
        })
      }
      if (onNodeDoubleClick) {
        renderer.on("doubleClickNode", ({ node }) => {
          const attrs = g.getNodeAttributes(node)
          onNodeDoubleClick({ id: node, ...attrs } as unknown as GraphNode)
        })
      }

      return () => {
        renderer.kill()
        sigmaRef.current = null
        graphRef.current = null
      }
    }, [data, layout, theme, onNodeClick, onNodeDoubleClick])

    // Container background
    const containerStyle: React.CSSProperties = {
      height,
      width: "100%",
      position: "relative",
      background: theme.backgroundColor,
      borderRadius: "var(--radius, 0.5rem)",
      overflow: "hidden",
    }

    return (
      <div className={className} style={containerStyle}>
        <div ref={containerRef} style={{ width: "100%", height: "100%" }} />
        {showLegend && nodeTypes.length > 0 && (
          <div
            style={{
              position: "absolute",
              top: 12,
              right: 12,
              background: "rgba(0,0,0,0.7)",
              backdropFilter: "blur(6px)",
              borderRadius: 8,
              padding: "10px 14px",
              fontSize: 12,
              display: "flex",
              flexDirection: "column",
              gap: 6,
            }}
          >
            {nodeTypes.map((nt) => (
              <div
                key={nt.type}
                style={{ display: "flex", alignItems: "center", gap: 8 }}
              >
                <span
                  style={{
                    width: 10,
                    height: 10,
                    borderRadius: "50%",
                    background: nt.color,
                    flexShrink: 0,
                  }}
                />
                <span style={{ color: theme.labelColor }}>{nt.label}</span>
              </div>
            ))}
          </div>
        )}
      </div>
    )
  }

  export { GraphContainer }

params_schema:
  {"output":"Componente GraphContainer que renderiza grafo interactivo con sigma.js, ForceAtlas2 layout y legend"}


id: input_ts_ui
name: input
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Input(props: InputHTMLAttributes): JSX.Element
tags: ["input","form","component","ui","interactive"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["@base-ui/react","react"]
tests: []
file_path: frontend/functions/ui/input.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onChange","onFocus","onBlur"]
framework: react
variant: ["default"]
content_hash: 00d4f8ff71dc5d74136f3fd7433e837a8f3cc5214fc6e72755765db175dc543d
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/input.tsx
description:
  Campo de entrada accesible con soporte para iconos, grupos, validación ARIA y estados disabled/invalid.

example:
  ```tsx
  <Input placeholder="Email" type="email" />
  <InputGroup>
    <InputIcon position="start"><SearchIcon /></InputIcon>
    <Input placeholder="Buscar..." />
  </InputGroup>
  ```

props:
  [{"name":"type","type":"string","required":false,"description":"Tipo de input HTML"},{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]

notes:
  Exporta Input, InputGroup e InputIcon. InputGroup detecta automáticamente la presencia de iconos y ajusta padding del Input.

code:
  import * as React from "react"
  import { Input as InputPrimitive } from "@base-ui/react/input"
  import { cn } from "../core/cn"

  function Input({ className, type, ...props }: React.ComponentProps<"input">) {
    return (
      <InputPrimitive
        type={type}
        data-slot="input"
        className={cn(
          "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
          "group-has-[data-slot=input-icon-start]/input-group:pl-9",
          "group-has-[data-slot=input-icon-end]/input-group:pr-9",
          className
        )}
        {...props}
      />
    )
  }

  interface InputGroupProps {
    children: React.ReactNode
    className?: string
  }

  function InputGroup({ children, className }: InputGroupProps) {
    return (
      <div data-slot="input-group" className={cn("group/input-group relative", className)}>
        {children}
      </div>
    )
  }

  interface InputIconProps {
    children: React.ReactNode
    position: "start" | "end"
    className?: string
  }

  function InputIcon({ children, position, className }: InputIconProps) {
    return (
      <span
        data-slot={`input-icon-${position}`}
        className={cn(
          "pointer-events-none absolute top-1/2 -translate-y-1/2 text-muted-foreground [&_svg]:size-4",
          position === "start" && "left-2.5",
          position === "end" && "right-2.5",
          className
        )}
      >
        {children}
      </span>
    )
  }

  export { Input, InputGroup, InputIcon }

params_schema:
  {"output":"Componente Input que renderiza campo de entrada accesible con soporte para iconos y validación ARIA"}


id: kpi_card_ts_ui
name: kpi_card
kind: component
lang: ts
domain: ui
version: 2.0.0
purity: impure
signature: KPICard(props: KPICardProps): JSX.Element
tags: ["kpi","card","metrics","dashboard","component","ui","sparkline"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/kpi_card.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["sm","default","lg"]
content_hash: 48133c381f3018bf2d384ba31f716b58f2df211a59d33fdfd234faac807e73d4
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/kpi-card.tsx
description:
  Card de KPI con label, valor+unidad, delta descriptivo con color semántico, icono, slot de chart inline y action. 3 tamaños.

example:
  ```tsx
  import { KPICard, Sparkline } from '@fn_library'

  {/* Básico */}
  <KPICard label="Revenue" value="$12,450" delta={{ value: 12.5, isPositive: true }} />

  {/* Con unidad separada, delta descriptivo, y mini barras */}
  <KPICard
    label="Processed Prompts"
    value="124"
    unit="k"
    icon={<ZapIcon className="h-4 w-4" />}
    delta={{ value: 15, isPositive: true, label: "Prompts Increased by", suffix: "vs yesterday" }}
    chart={<Sparkline data={[5, 8, 3, 9, 6, 12, 7]} variant="bar" colors={['#3b82f6', '#8b5cf6', '#f59e0b', '#10b981', '#ef4444', '#ec4899', '#06b6d4']} height={32} />}
    action={<button className="text-muted-foreground hover:text-foreground">...</button>}
  />

  {/* Dashboard dark sin bordes */}
  <KPICard label="Sessions" value={9821} className="border-0 shadow-none" />
  ```

props:
  [{"name":"label","type":"string","required":true,"description":"Etiqueta del KPI"},{"name":"value","type":"string | number","required":true,"description":"Valor principal"},{"name":"unit","type":"string","required":false,"description":"Unidad junto al valor en font menor (ej: k, ms, %)"},{"name":"delta","type":"{ value: number; isPositive: boolean; label?: string; suffix?: string }","required":false,"description":"Cambio con dirección, label descriptivo y sufijo"},{"name":"icon","type":"ReactNode","required":false,"description":"Icono a la izquierda del label"},{"name":"action","type":"ReactNode","required":false,"description":"Slot top-right para menú o acciones"},{"name":"chart","type":"ReactNode","required":false,"description":"Slot para mini chart inline junto al valor"},{"name":"size","type":"'sm' | 'default' | 'lg'","required":false,"description":"Tamaño"}]

notes:
  - El icono ahora se renderiza a la **izquierda** del label (antes estaba a la derecha).
  - `unit` separa la unidad del valor con font menor para el efecto "124 k" del diseño.
  - `delta.label` y `delta.suffix` permiten texto descriptivo: "Increased by ▲ +15% vs yesterday".
  - `chart` es un slot genérico — pasar un `<Sparkline variant="bar" colors={[...]} />` para mini barras multicolor.
  - `action` es un slot top-right para menú contextual.
  - Usa `cn()` para merge de clases. `className="border-0 shadow-none"` para dashboards dark.

code:
  import * as React from 'react'
  import { cn } from '../core/cn'

  type KPICardSize = 'sm' | 'default' | 'lg'

  interface Delta {
    value: number
    isPositive: boolean
    /** Descriptive label before value, e.g. "Increased by" */
    label?: string
    /** Suffix after value, e.g. "vs yesterday" */
    suffix?: string
  }

  interface KPICardProps extends React.HTMLAttributes<HTMLDivElement> {
    label: string
    value: string | number
    /** Unit displayed next to value in smaller font, e.g. "k", "ms", "%" */
    unit?: string
    delta?: Delta
    icon?: React.ReactNode
    /** Action slot rendered top-right, e.g. a menu button */
    action?: React.ReactNode
    /** Inline chart slot rendered to the right of the value */
    chart?: React.ReactNode
    subtitle?: string
    size?: KPICardSize
  }

  const sizeStyles: Record<KPICardSize, { value: string; unit: string; label: string }> = {
    sm: { value: 'text-2xl font-bold', unit: 'text-base font-medium', label: 'text-xs' },
    default: { value: 'text-3xl font-bold', unit: 'text-lg font-medium', label: 'text-sm' },
    lg: { value: 'text-4xl font-bold', unit: 'text-xl font-medium', label: 'text-base' },
  }

  const KPICard = React.forwardRef<HTMLDivElement, KPICardProps>(
    ({ label, value, unit, delta, icon, action, chart, subtitle, size = 'default', className, ...props }, ref) => {
      const styles = sizeStyles[size]
      const deltaColor = delta
        ? delta.value === 0 ? 'text-muted-foreground'
          : delta.isPositive ? 'text-green-600 dark:text-green-500'
          : 'text-red-600 dark:text-red-500'
        : ''

      return (
        <div ref={ref} className={cn('rounded-lg border bg-card p-4 text-card-foreground shadow-sm', className)} {...props}>
          <div className="flex items-start justify-between">
            <div className="flex items-center gap-2">
              {icon && <div className="text-muted-foreground">{icon}</div>}
              <div className="space-y-1">
                <p className={cn('text-muted-foreground', styles.label)}>{label}</p>
                {subtitle && <p className="text-xs text-muted-foreground/80">{subtitle}</p>}
              </div>
            </div>
            {action && <div className="text-muted-foreground">{action}</div>}
          </div>
          <div className="mt-3 flex items-end justify-between gap-4">
            <div className="space-y-1">
              <div className="flex items-baseline gap-1">
                <span className={cn('tracking-tight', styles.value)}>{value}</span>
                {unit && <span className={cn('text-muted-foreground', styles.unit)}>{unit}</span>}
              </div>
              {delta && (
                <div className="flex items-center gap-1 text-xs text-muted-foreground">
                  {delta.label && <span>{delta.label}</span>}
                  <span className={cn('font-medium', deltaColor)}>
                    {delta.isPositive ? '▲' : '▼'} {delta.value > 0 ? '+' : ''}{delta.value}{delta.label ? '' : '%'}
                  </span>
                  {delta.suffix && <span>{delta.suffix}</span>}
                </div>
              )}
            </div>
            {chart && <div className="flex-shrink-0">{chart}</div>}
          </div>
        </div>
      )
    }
  )
  KPICard.displayName = 'KPICard'

  export { KPICard }
  export type { KPICardProps, Delta, KPICardSize }

params_schema:
  {"output":"Componente KPICard que renderiza card de métrica con label, valor, delta descriptivo, icono y slot de mini chart"}


id: label_ts_ui
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_ts_core"]
uses_types: []
returns: []
imports: ["react"]
example:
  ```tsx
  <Label htmlFor="email">Email</Label>
  ```

tests: []
file_path: frontend/functions/ui/label.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: [{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]
emits: []
framework: react
variant: ["default"]
content_hash: de5cfff7f90daae36d60875437f10650a4d9eaad6fd802e1b5dedcee249a5e1f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/label.tsx
code:
  import * as React from "react"
  import { cn } from "../core/cn"

  function Label({ className, ...props }: React.ComponentProps<"label">) {
    return (
      <label
        data-slot="label"
        className={cn(
          "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
          className
        )}
        {...props}
      />
    )
  }

  export { Label }

params_schema:
  {"output":"Componente Label que renderiza etiqueta de formulario accesible con soporte para estados disabled"}


id: line_chart_ts_ui
name: line_chart
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: LineChart(props: LineChartProps): JSX.Element
tags: ["chart","line","visualization","recharts","component","ui"]
uses_functions: ["cn_ts_core","chart_container_ts_ui","get_series_color_ts_core"]
uses_types: ["ChartSeries_ts_ui"]
returns: []
imports: ["recharts"]
tests: []
file_path: frontend/functions/ui/line_chart.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["default"]
content_hash: 7baf0393f094a6202d7f9e601aeba9c681269a8ed0c2dad5a05bf12e37ea514b
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/charts/line-chart.tsx
description:
  Gráfico de líneas Recharts con multi-series, 5 tipos de curva, zoom brush, líneas de referencia, tooltips temáticos.

example:
  ```tsx
  <LineChart
    data={salesData}
    xKey="month"
    series={[
      { key: "revenue", name: "Revenue", color: "#3b82f6" },
      { key: "cost", name: "Cost", color: "#ef4444" },
    ]}
    zoomable
    showLegend
  />
  ```

props:
  [{"name":"data","type":"Record\u003cstring, unknown\u003e[]","required":true,"description":"Array de datos"},{"name":"xKey","type":"string","required":true,"description":"Key del eje X"},{"name":"series","type":"Series[]","required":false,"description":"Series de datos"},{"name":"zoomable","type":"boolean","required":false,"description":"Habilitar zoom brush"},{"name":"curveType","type":"'linear' | 'monotone' | 'step' | 'stepBefore' | 'stepAfter'","required":false,"description":"Tipo de curva (default monotone)"},{"name":"referenceLines","type":"Array\u003c{ y: number; label?: string; color?: string }\u003e","required":false,"description":"Líneas de referencia horizontales"}]

code:
  import {
    LineChart as RechartsLineChart, Line, XAxis, YAxis, CartesianGrid,
    Tooltip, Legend, Brush, ReferenceLine,
  } from 'recharts'
  import { ChartContainer, ChartTooltipContent, type Series, getSeriesColor } from './chart_container'

  type CurveType = 'linear' | 'monotone' | 'step' | 'stepBefore' | 'stepAfter'

  interface LineChartProps {
    data: Record<string, unknown>[]
    xKey: string
    yKey?: string
    series?: Series[]
    curveType?: CurveType
    showGrid?: boolean
    showLegend?: boolean
    showDots?: boolean
    zoomable?: boolean
    height?: number | string
    className?: string
    xAxisFormatter?: (value: unknown) => string
    yAxisFormatter?: (value: unknown) => string
    valueFormatter?: (value: number) => string
    referenceLines?: Array<{ y: number; label?: string; color?: string }>
  }

  function LineChartComponent({
    data, xKey, yKey, series, curveType = 'monotone', showGrid = true, showLegend = false,
    showDots = true, zoomable = false, height = 300, className, xAxisFormatter, yAxisFormatter,
    valueFormatter = (v) => v.toLocaleString(), referenceLines = [],
  }: LineChartProps) {
    const lines = series
      ? series.map((s, i) => ({ dataKey: s.key, name: s.name, stroke: getSeriesColor(i, s.color) }))
      : yKey ? [{ dataKey: yKey, name: yKey, stroke: getSeriesColor(0) }] : []

    return (
      <ChartContainer className={className} height={height}>
        <RechartsLineChart data={data} margin={{ top: 10, right: 10, left: 10, bottom: zoomable ? 30 : 10 }}>
          {showGrid && <CartesianGrid strokeDasharray="3 3" className="stroke-muted" />}
          <XAxis dataKey={xKey} tickFormatter={xAxisFormatter} className="text-xs fill-muted-foreground" />
          <YAxis tickFormatter={yAxisFormatter} className="text-xs fill-muted-foreground" />
          <Tooltip content={<ChartTooltipContent valueFormatter={valueFormatter} />} cursor={{ stroke: 'hsl(var(--muted-foreground))', strokeDasharray: '3 3' }} />
          {showLegend && <Legend />}
          {referenceLines.map((ref, i) => (
            <ReferenceLine key={i} y={ref.y} stroke={ref.color || 'hsl(var(--muted-foreground))'} strokeDasharray="3 3" label={ref.label ? { value: ref.label, position: 'right' } : undefined} />
          ))}
          {lines.map((line) => (
            <Line key={line.dataKey} type={curveType} dataKey={line.dataKey} name={line.name} stroke={line.stroke} strokeWidth={2} dot={showDots ? { r: 3, fill: line.stroke } : false} activeDot={{ r: 5, fill: line.stroke }} />
          ))}
          {zoomable && <Brush dataKey={xKey} height={20} stroke="hsl(var(--primary))" fill="hsl(var(--muted))" tickFormatter={xAxisFormatter} />}
        </RechartsLineChart>
      </ChartContainer>
    )
  }

  export const LineChart = LineChartComponent
  export type { LineChartProps, CurveType }

params_schema:
  {"output":"Componente LineChart que renderiza gráfico de líneas multi-series con zoom, curvas customizables y líneas de referencia"}


id: page_header_ts_ui
name: page_header
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: PageHeader(props: PageHeaderProps): JSX.Element
tags: ["header","page","layout","navigation","component","ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/page_header.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onBack","onTabChange"]
framework: react
variant: ["full","simple"]
content_hash: a6f066b49953232883e0221a7c5de2cab769b6ad569616babd3335891f0e89e7
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/page-header.tsx
description:
  Cabecera de página con título, subtítulo, acciones, back button, tabs integrados, badge y modo sticky. Incluye SimplePageHeader.

example:
  ```tsx
  <PageHeader
    title="Dashboard"
    subtitle="Vista general"
    actions={<Button>Export</Button>}
    tabs={[{ label: "Overview", value: "overview" }, { label: "Analytics", value: "analytics" }]}
    activeTab="overview"
    onTabChange={setTab}
  />
  ```

props:
  [{"name":"title","type":"string","required":true,"description":"Título principal"},{"name":"subtitle","type":"string","required":false,"description":"Subtítulo"},{"name":"actions","type":"ReactNode","required":false,"description":"Botones de acción"},{"name":"tabs","type":"TabItem[]","required":false,"description":"Tabs de navegación integrados"},{"name":"sticky","type":"boolean","required":false,"description":"Header fijo al scroll"}]

code:
  "use client"

  import * as React from "react"
  import { cn } from "../core/cn"

  interface TabItem {
    label: string
    value: string
    icon?: React.ReactNode
    disabled?: boolean
  }

  interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
    title: string
    subtitle?: string
    actions?: React.ReactNode
    onBack?: () => void
    tabs?: TabItem[]
    activeTab?: string
    onTabChange?: (value: string) => void
    badge?: React.ReactNode
    sticky?: boolean
  }

  function PageHeader({
    title, subtitle, actions, onBack, tabs, activeTab, onTabChange,
    badge, sticky = false, className, ...props
  }: PageHeaderProps) {
    return (
      <header
        data-slot="page-header"
        className={cn("space-y-4 border-b bg-background pb-4", sticky && "sticky top-0 z-20", className)}
        {...props}
      >
        <div className="flex items-start justify-between gap-4">
          <div className="flex items-start gap-3">
            {onBack && (
              <button onClick={onBack} className="mt-1 inline-flex size-7 shrink-0 items-center justify-center rounded-md hover:bg-muted">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m15 18-6-6 6-6"/></svg>
              </button>
            )}
            <div className="space-y-1">
              <div className="flex items-center gap-3">
                <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
                {badge}
              </div>
              {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
            </div>
          </div>
          {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
        </div>
        {tabs && tabs.length > 0 && (
          <nav className="flex gap-4 border-b -mb-4 pb-0">
            {tabs.map((tab) => (
              <button
                key={tab.value}
                type="button"
                disabled={tab.disabled}
                onClick={() => onTabChange?.(tab.value)}
                className={cn(
                  "inline-flex items-center gap-2 border-b-2 px-1 pb-3 text-sm font-medium transition-colors",
                  activeTab === tab.value ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground",
                  tab.disabled && "pointer-events-none opacity-50"
                )}
              >
                {tab.icon && <span className="size-4">{tab.icon}</span>}
                {tab.label}
              </button>
            ))}
          </nav>
        )}
      </header>
    )
  }

  interface SimplePageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
    title: string
    description?: string
    children?: React.ReactNode
  }

  function SimplePageHeader({ title, description, children, className, ...props }: SimplePageHeaderProps) {
    return (
      <div className={cn("flex items-center justify-between border-b pb-4", className)} {...props}>
        <div className="space-y-1">
          <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
          {description && <p className="text-sm text-muted-foreground">{description}</p>}
        </div>
        {children && <div className="flex items-center gap-2">{children}</div>}
      </div>
    )
  }

  export { PageHeader, SimplePageHeader }

params_schema:
  {"output":"Componente PageHeader que renderiza cabecera de página con título, acciones, tabs integrados y modo sticky"}


id: pagination_ts_ui
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: []
imports: ["lucide-react","./button"]
tests: []
file_path: frontend/functions/ui/pagination.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: [{"name":"className","type":"string","required":false,"description":"Clases CSS adicionales"}]
emits: []
framework: react
variant: []
content_hash: b907ce14ec1510e790172cafc3f1f1cf91c04003a4c1893aae3c50daeedbd929
example:
  ```tsx
  <Pagination>
    <PaginationContent>
      <PaginationItem>
        <PaginationPrevious href="/page/1" />
      </PaginationItem>
      <PaginationItem>
        <PaginationLink href="/page/1">1</PaginationLink>
      </PaginationItem>
      <PaginationItem>
        <PaginationLink href="/page/2" isActive>2</PaginationLink>
      </PaginationItem>
      <PaginationItem>
        <PaginationLink href="/page/3">3</PaginationLink>
      </PaginationItem>
      <PaginationItem>
        <PaginationEllipsis />
      </PaginationItem>
      <PaginationItem>
        <PaginationNext href="/page/3" />
      </PaginationItem>
    </PaginationContent>
  </Pagination>
  ```

notes:
  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.

code:
  import * as React from "react"
  import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
  import { cn } from "../core/cn"
  import { buttonVariants } from "./button"

  function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
    return (
      <nav
        data-slot="pagination"
        role="navigation"
        aria-label="pagination"
        className={cn("mx-auto flex w-full justify-center", className)}
        {...props}
      />
    )
  }

  function PaginationContent({ className, ...props }: React.ComponentProps<"ul">) {
    return (
      <ul
        data-slot="pagination-content"
        className={cn("flex flex-row items-center gap-1", className)}
        {...props}
      />
    )
  }

  function PaginationItem({ ...props }: React.ComponentProps<"li">) {
    return <li data-slot="pagination-item" {...props} />
  }

  type PaginationLinkProps = {
    isActive?: boolean
    disabled?: boolean
    size?: "icon" | "default" | "sm" | "lg"
  } & React.ComponentProps<"a">

  function PaginationLink({ className, isActive, disabled, size = "icon", ...props }: PaginationLinkProps) {
    return (
      <a
        data-slot="pagination-link"
        aria-current={isActive ? "page" : undefined}
        aria-disabled={disabled}
        className={cn(
          buttonVariants({ variant: isActive ? "outline" : "ghost", size }),
          disabled && "pointer-events-none opacity-50",
          isActive && "border-border font-medium",
          className
        )}
        {...props}
      />
    )
  }

  function PaginationPrevious({ className, ...props }: React.ComponentProps<"a">) {
    return (
      <PaginationLink
        data-slot="pagination-previous"
        aria-label="Go to previous page"
        size="default"
        className={cn("gap-1 px-2.5 pl-2", className)}
        {...props}
      >
        <ChevronLeftIcon className="size-4" />
        <span>Previous</span>
      </PaginationLink>
    )
  }

  function PaginationNext({ className, ...props }: React.ComponentProps<"a">) {
    return (
      <PaginationLink
        data-slot="pagination-next"
        aria-label="Go to next page"
        size="default"
        className={cn("gap-1 px-2.5 pr-2", className)}
        {...props}
      >
        <span>Next</span>
        <ChevronRightIcon className="size-4" />
      </PaginationLink>
    )
  }

  function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">) {
    return (
      <span
        data-slot="pagination-ellipsis"
        aria-hidden
        className={cn("flex size-8 items-center justify-center text-muted-foreground", className)}
        {...props}
      >
        <MoreHorizontalIcon className="size-4" />
        <span className="sr-only">More pages</span>
      </span>
    )
  }

  export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious }
  export type { PaginationLinkProps }

params_schema:
  {"output":"Componente Pagination que renderiza controles de navegación de páginas con Previous/Next y números"}


id: pie_chart_ts_ui
name: pie_chart
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: PieChart(props: PieChartProps): JSX.Element
tags: ["chart","pie","donut","visualization","recharts","component","ui","dashboard"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["recharts"]
tests: []
file_path: frontend/functions/ui/pie_chart.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["pie","donut"]
content_hash: cb7a1bf7e22981a937ac22ef4562281eca610a5279cae09673aa99bbbfe6c60f
description:
  Gráfico de torta/dona Recharts con Cell por segmento, colores automáticos, labels con porcentaje, Legend y Tooltip temático. Soporte donut con innerRadius configurable.

example:
  ```tsx
  // Pie simple
  <PieChart
    data={[{ lang: 'Go', count: 42 }, { lang: 'Python', count: 28 }, { lang: 'Bash', count: 15 }]}
    nameKey="lang"
    valueKey="count"
  />

  // Dona sin labels
  <PieChart
    data={distributions}
    nameKey="domain"
    valueKey="functions"
    donut
    showLabels={false}
    valueFormatter={(v) => `${v} fns`}
  />
  ```

props:
  [{"name":"data","type":"Record\u003cstring, unknown\u003e[]","required":true,"description":"Array de datos. Los valores de valueKey se convierten a number automáticamente."},{"name":"nameKey","type":"string","required":true,"description":"Key del campo que contiene el nombre/etiqueta de cada segmento"},{"name":"valueKey","type":"string","required":true,"description":"Key del campo numérico que determina el tamaño de cada segmento"},{"name":"colors","type":"string[]","required":false,"description":"Paleta de colores hex. Default: 8 colores accesibles. Se repite cíclicamente."},{"name":"donut","type":"boolean","required":false,"description":"Modo dona. innerRadius pasa a 50 por defecto cuando donut=true."},{"name":"innerRadius","type":"number","required":false,"description":"Radio interno en px. Sobreescribe el default calculado por donut."},{"name":"outerRadius","type":"number","required":false,"description":"Radio externo en px. Default 100."},{"name":"showLegend","type":"boolean","required":false,"description":"Mostrar leyenda. Default true."},{"name":"showLabels","type":"boolean","required":false,"description":"Mostrar labels nombre+% en cada segmento. Default true."},{"name":"height","type":"number | string","required":false,"description":"Altura del contenedor. Default 300."},{"name":"valueFormatter","type":"(value: number) =\u003e string","required":false,"description":"Formateador de valores para el tooltip. Default toLocaleString."}]

notes:
  Extraido y generalizado desde `apps/rapid_dashboards/frontend/src/components/widgets/PieChartWidget.tsx`. Los valores de `valueKey` se convierten a `Number()` para garantizar que Recharts los interprete correctamente (útil cuando los datos vienen de SQLite como strings). El `ResponsiveContainer` ocupa el 100% del ancho del padre.

code:
  import {
    PieChart as RechartsPieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer,
  } from 'recharts'
  import { cn } from '../core/cn'

  const DEFAULT_COLORS = [
    '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6',
    '#ec4899', '#06b6d4', '#f97316',
  ]

  interface PieChartProps {
    data: Record<string, unknown>[]
    nameKey: string
    valueKey: string
    colors?: string[]
    donut?: boolean
    innerRadius?: number
    outerRadius?: number
    showLegend?: boolean
    showLabels?: boolean
    height?: number | string
    className?: string
    valueFormatter?: (value: number) => string
  }

  function PieChartComponent({
    data,
    nameKey,
    valueKey,
    colors = DEFAULT_COLORS,
    donut = false,
    innerRadius,
    outerRadius = 100,
    showLegend = true,
    showLabels = true,
    height = 300,
    className,
    valueFormatter = (v) => v.toLocaleString(),
  }: PieChartProps) {
    // Ensure numeric values for Recharts Pie
    const pieData = data.map(row => ({
      ...row,
      [valueKey]: Number(row[valueKey]) || 0,
    }))

    const resolvedInnerRadius = donut ? (innerRadius ?? 50) : (innerRadius ?? 0)

    const labelRenderer = showLabels
      ? ({ name, percent }: Record<string, unknown>) =>
          `${name ?? ''} ${(((percent as number) ?? 0) * 100).toFixed(0)}%`
      : undefined

    return (
      <ResponsiveContainer width="100%" height={height} className={cn(className)}>
        <RechartsPieChart>
          <Pie
            data={pieData}
            dataKey={valueKey}
            nameKey={nameKey}
            cx="50%"
            cy="50%"
            outerRadius={outerRadius}
            innerRadius={resolvedInnerRadius}
            strokeWidth={0}
            fontSize={11}
            label={labelRenderer}
            labelLine={showLabels}
          >
            {pieData.map((_, i) => (
              <Cell key={i} fill={colors[i % colors.length]} />
            ))}
          </Pie>
          <Tooltip
            contentStyle={{
              backgroundColor: 'var(--card)',
              border: '1px solid var(--border)',
              borderRadius: 8,
              fontSize: 12,
            }}
            formatter={(value: unknown) => [valueFormatter(value as number)]}
          />
          {showLegend && <Legend wrapperStyle={{ fontSize: 11 }} />}
        </RechartsPieChart>
      </ResponsiveContainer>
    )
  }

  export const PieChart = PieChartComponent
  export type { PieChartProps }

params_schema:
  {"output":"Componente PieChart que renderiza gráfico de torta o dona con labels de porcentaje, leyenda y tooltip"}


id: popover_ts_ui
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: []
imports: ["@base-ui/react/popover"]
tests: []
file_path: frontend/functions/ui/popover.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onOpenChange"]
framework: react
variant: []
content_hash: 243824240e69103eb6f172c95f859128b53eb429570cf883cdbf7c1e6d97ace1
example:
  ```tsx
  <Popover>
    <PopoverTrigger asChild>
      <Button variant="outline">Abrir</Button>
    </PopoverTrigger>
    <PopoverContent>
      <PopoverHeader>
        <PopoverTitle>Configuracion</PopoverTitle>
        <PopoverDescription>Ajusta tus preferencias.</PopoverDescription>
      </PopoverHeader>
      <div className="mt-4">
        {/* contenido */}
      </div>
    </PopoverContent>
  </Popover>
  ```

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) =\u003e 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)"}]

notes:
  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.

code:
  import * as React from "react"
  import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
  import { cn } from "../core/cn"

  function Popover({ ...props }: PopoverPrimitive.Root.Props) {
    return <PopoverPrimitive.Root data-slot="popover" {...props} />
  }

  function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
    return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
  }

  function PopoverPortal({ ...props }: PopoverPrimitive.Portal.Props) {
    return <PopoverPrimitive.Portal data-slot="popover-portal" {...props} />
  }

  function PopoverContent({ className, sideOffset = 4, ...props }: PopoverPrimitive.Positioner.Props) {
    return (
      <PopoverPortal>
        <PopoverPrimitive.Positioner
          data-slot="popover-content"
          sideOffset={sideOffset}
          className={cn("z-50", className)}
          {...props}
        >
          <PopoverPrimitive.Popup
            className={cn(
              "w-72 rounded-xl border bg-popover p-4 text-popover-foreground shadow-md outline-none",
              "data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
              "data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
              "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
            )}
          >
            {props.children}
          </PopoverPrimitive.Popup>
        </PopoverPrimitive.Positioner>
      </PopoverPortal>
    )
  }

  function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
    return <PopoverPrimitive.Close data-slot="popover-close" {...props} />
  }

  function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
    return <div data-slot="popover-header" className={cn("flex flex-col gap-1.5", className)} {...props} />
  }

  function PopoverTitle({ className, ...props }: React.ComponentProps<"h4">) {
    return <h4 data-slot="popover-title" className={cn("text-sm font-semibold leading-none", className)} {...props} />
  }

  function PopoverDescription({ className, ...props }: React.ComponentProps<"p">) {
    return <p data-slot="popover-description" className={cn("text-sm text-muted-foreground", className)} {...props} />
  }

  export { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverPortal, PopoverTitle, PopoverTrigger }

params_schema:
  {"output":"Componente Popover que renderiza contenido flotante accesible posicionado automáticamente"}


id: progress_bar_ts_ui
name: progress_bar
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: ProgressBar(props: ProgressBarProps): JSX.Element
tags: ["progress","loading","component","ui","feedback"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["class-variance-authority"]
tests: []
file_path: frontend/functions/ui/progress_bar.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: ["primary","success","warning","destructive"]
content_hash: 02f7353299fcc14c83fe7a7d47021d6eaa7f2da3b2f0c9cca2b8ee017a9c5fae
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/progress/progress-bar.tsx
description:
  Barra de progreso con variantes de color y tamaño, buffer, animación, modo indeterminado y display de valor.

example:
  ```tsx
  <ProgressBar value={75} color="success" showValue />
  <ProgressBar value={0} indeterminate />
  <ProgressBar value={50} buffer={80} animated />
  <ProgressBar value={30} size="lg" color="warning" />
  ```

props:
  [{"name":"value","type":"number","required":true,"description":"Valor actual de progreso"},{"name":"max","type":"number","required":false,"description":"Valor máximo (default 100)"},{"name":"buffer","type":"number","required":false,"description":"Valor de buffer secundario (opcional)"},{"name":"showValue","type":"boolean","required":false,"description":"Mostrar porcentaje como texto superpuesto"},{"name":"animated","type":"boolean","required":false,"description":"Activar animación de rayas (stripes)"},{"name":"indeterminate","type":"boolean","required":false,"description":"Modo indeterminado sin valor conocido"},{"name":"size","type":"'sm' | 'md' | 'lg'","required":false,"description":"Altura de la barra (default md)"},{"name":"color","type":"'primary' | 'success' | 'warning' | 'destructive'","required":false,"description":"Color semántico (default primary)"},{"name":"label","type":"string","required":false,"description":"aria-label para accesibilidad"}]

notes:
  El porcentaje se clampea a [0, 100] internamente. El buffer se renderiza como capa semitransparente detrás del fill.
  Las animaciones de stripes e indeterminate requieren keyframes definidos en el CSS global:
  - `progress-stripes`: background-position de 0 a 1rem
  - `progress-indeterminate`: translateX de -100% a 300%

code:
  import { cva, type VariantProps } from 'class-variance-authority'
  import { cn } from '../core/cn'

  const progressBarVariants = cva(
    'relative w-full overflow-hidden rounded-full bg-muted',
    {
      variants: {
        size: { sm: 'h-1', md: 'h-2', lg: 'h-3' },
        color: {
          primary: '[&_.progress-fill]:bg-primary',
          success: '[&_.progress-fill]:bg-emerald-500',
          warning: '[&_.progress-fill]:bg-amber-500',
          destructive: '[&_.progress-fill]:bg-destructive',
        },
      },
      defaultVariants: { size: 'md', color: 'primary' },
    }
  )

  export interface ProgressBarProps extends VariantProps<typeof progressBarVariants> {
    value: number
    max?: number
    buffer?: number
    showValue?: boolean
    animated?: boolean
    indeterminate?: boolean
    label?: string
    className?: string
  }

  export function ProgressBar({
    value, max = 100, buffer, showValue = false, animated = false,
    indeterminate = false, size = 'md', color = 'primary', label, className,
  }: ProgressBarProps) {
    const percentage = Math.min(100, Math.max(0, (value / max) * 100))
    const bufferPercentage = buffer ? Math.min(100, Math.max(0, (buffer / max) * 100)) : undefined

    return (
      <div
        data-slot="progress-bar"
        role="progressbar"
        aria-valuenow={indeterminate ? undefined : value}
        aria-valuemin={0}
        aria-valuemax={max}
        aria-label={label}
        className={cn(progressBarVariants({ size, color }), className)}
      >
        {bufferPercentage !== undefined && (
          <div className="progress-buffer absolute inset-0 bg-current opacity-20 transition-all duration-300" style={{ width: `${bufferPercentage}%` }} />
        )}
        <div
          className={cn(
            'progress-fill h-full transition-all duration-300',
            animated && 'bg-[length:1rem_1rem] bg-[linear-gradient(45deg,rgba(255,255,255,.15)_25%,transparent_25%,transparent_50%,rgba(255,255,255,.15)_50%,rgba(255,255,255,.15)_75%,transparent_75%,transparent)] animate-[progress-stripes_1s_linear_infinite]',
            indeterminate && 'w-1/3 animate-[progress-indeterminate_1.5s_ease-in-out_infinite]'
          )}
          style={indeterminate ? undefined : { width: `${percentage}%` }}
        />
        {showValue && !indeterminate && (
          <span className="absolute inset-0 flex items-center justify-center text-[10px] font-medium text-foreground mix-blend-difference">
            {Math.round(percentage)}%
          </span>
        )}
      </div>
    )
  }

  export { progressBarVariants }

params_schema:
  {"output":"Componente ProgressBar que renderiza barra de progreso con animaciones, buffer y modo indeterminado"}


id: radio_group_ts_ui
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: []
imports: ["@base-ui/react/radio-group","@base-ui/react/radio"]
tests: []
file_path: frontend/functions/ui/radio_group.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onValueChange"]
framework: react
variant: []
content_hash: be6060790b9226e43cb632530823215818e70cf24fd6c14d4b9053e4d4969cfd
params_schema: {"output":"Componente RadioGroup que renderiza grupo de opciones exclusivas accesible"}
example:
  ```tsx
  <RadioGroup defaultValue="option-a">
    <RadioGroupItem value="option-a" label="Opcion A" />
    <RadioGroupItem value="option-b" label="Opcion B" />
    <RadioGroupItem value="option-c" label="Opcion C" disabled />
  </RadioGroup>
  ```

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) =\u003e 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"}]

notes:
  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.

code:
  import * as React from "react"
  import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
  import { Radio } from "@base-ui/react/radio"
  import { cn } from "../core/cn"

  function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
    return (
      <RadioGroupPrimitive
        data-slot="radio-group"
        className={cn("grid gap-2", className)}
        {...props}
      />
    )
  }

  interface RadioGroupItemProps extends Radio.Root.Props {
    label?: string
    className?: string
    labelClassName?: string
  }

  function RadioGroupItem({ className, label, id, labelClassName, ...props }: RadioGroupItemProps) {
    const internalId = React.useId()
    const itemId = id ?? internalId

    return (
      <div className="flex items-center gap-2">
        <Radio.Root
          id={itemId}
          data-slot="radio-group-item"
          className={cn(
            "aspect-square size-4 rounded-full border border-input bg-transparent transition-colors outline-none",
            "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
            "data-checked:border-primary",
            "disabled:pointer-events-none disabled:opacity-50",
            className
          )}
          {...props}
        >
          <Radio.Indicator
            data-slot="radio-group-indicator"
            className="flex items-center justify-center"
          >
            <span className="block size-2 rounded-full bg-primary" />
          </Radio.Indicator>
        </Radio.Root>
        {label && (
          <label
            htmlFor={itemId}
            data-slot="radio-group-label"
            className={cn("text-sm font-medium leading-none cursor-pointer select-none", labelClassName)}
          >
            {label}
          </label>
        )}
      </div>
    )
  }

  export { RadioGroup, RadioGroupItem }
  export type { RadioGroupItemProps }


id: search_bar_ts_ui
name: search_bar
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: SearchBar(props: SearchBarProps): JSX.Element
description: Search input with debounce, search icon, and clear button
tags: ["component","ui","search","input","debounce"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["lucide-react"]
tests: []
file_path: frontend/functions/ui/search_bar.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: []
framework: react
variant: []
content_hash: c16f6911ac3a467ea503dabd159134ed84bf8abb00a09e887405b176bd8ba05a
example:
  ```tsx
  import { SearchBar } from '@fn_library'

  function MyPage() {
    return (
      <SearchBar
        onSearch={(query) => console.log('search:', query)}
        placeholder="Search entities..."
        debounceMs={300}
      />
    )
  }
  ```

props:
  [{"name":"onSearch","type":"(query: string) =\u003e void","required":true,"description":"Called with the debounced search query"},{"name":"placeholder","type":"string","required":false,"description":"Placeholder text (default: Search...)"},{"name":"debounceMs","type":"number","required":false,"description":"Debounce delay in ms (default: 300)"},{"name":"className","type":"string","required":false,"description":"Additional CSS classes"}]

notes:
  - Debounce usa ref para evitar re-renders innecesarios del callback
  - El icono de clear solo aparece cuando hay texto
  - Usa CSS variables del tema para colores (border, input, foreground, muted-foreground)

code:
  import * as React from "react"
  import { cn } from "../core/cn"
  import { Search, X } from "lucide-react"

  interface SearchBarProps {
    /** Called with the debounced search query */
    onSearch: (query: string) => void
    /** Placeholder text */
    placeholder?: string
    /** Debounce delay in ms (default 300) */
    debounceMs?: number
    /** Additional CSS classes for the outer wrapper */
    className?: string
  }

  function SearchBar({
    onSearch,
    placeholder = "Search...",
    debounceMs = 300,
    className,
  }: SearchBarProps) {
    const [query, setQuery] = React.useState("")
    const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
    const onSearchRef = React.useRef(onSearch)
    onSearchRef.current = onSearch

    React.useEffect(() => {
      if (timerRef.current) clearTimeout(timerRef.current)
      timerRef.current = setTimeout(() => {
        onSearchRef.current(query)
      }, debounceMs)
      return () => {
        if (timerRef.current) clearTimeout(timerRef.current)
      }
    }, [query, debounceMs])

    return (
      <div
        className={cn(
          "flex flex-1 items-center gap-2 rounded border border-border bg-input px-2 py-1",
          className,
        )}
      >
        <Search size={14} className="text-muted-foreground shrink-0" />
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder={placeholder}
          className="flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
        />
        {query && (
          <button
            onClick={() => setQuery("")}
            className="p-0.5 text-muted-foreground hover:text-foreground"
            aria-label="Clear search"
          >
            <X size={12} />
          </button>
        )}
      </div>
    )
  }

  export { SearchBar }
  export type { SearchBarProps }

params_schema:
  {"params":[{"name":"onSearch","desc":"Callback que se ejecuta con la query debounceada"},{"name":"placeholder","desc":"Placeholder del input (por defecto 'Search...')"},{"name":"debounceMs","desc":"Delay en milisegundos para el debounce (por defecto 300)"}],"output":"Componente SearchBar que renderiza input de búsqueda con icono y botón de limpiar"}


id: select_ts_ui
name: select
kind: component
lang: ts
domain: ui
version: 1.0.0
purity: impure
signature: Select<T>(props: SelectRootProps<T>): JSX.Element
tags: ["select","form","dropdown","component","ui","interactive"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["@base-ui/react","lucide-react","react"]
tests: []
file_path: frontend/functions/ui/select.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
emits: ["onValueChange"]
framework: react
variant: ["default"]
content_hash: c8193731b5f539761db855d679a81723d44b1bddfd633c1a3e8f9e104a643e0b
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
source_file: frontend/src/components/ui/select.tsx
description:
  Select genérico accesible con grupos, separadores y animaciones. Base-UI primitive con posicionamiento automático.

example:
  ```tsx
  <Select>
    <SelectTrigger><SelectValue placeholder="Elegir..." /></SelectTrigger>
    <SelectContent>
      <SelectGroup>
        <SelectGroupLabel>Frutas</SelectGroupLabel>
        <SelectItem value="apple">Manzana</SelectItem>
        <SelectItem value="banana">Banana</SelectItem>
      </SelectGroup>
      <SelectSeparator />
      <SelectItem value="other">Otro</SelectItem>
    </SelectContent>
  </Select>
  ```

props:
  [{"name":"value","type":"T","required":false,"description":"Valor seleccionado (controlled)"},{"name":"onValueChange","type":"(value: T) =\u003e void","required":false,"description":"Callback al cambiar selección"},{"name":"defaultValue","type":"T","required":false,"description":"Valor inicial (uncontrolled)"},{"name":"disabled","type":"boolean","required":false,"description":"Deshabilitar el select"}]

notes:
  Exporta 9 subcomponentes composables: Select, SelectTrigger, SelectValue, SelectPortal, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectSeparator.
  Genérico sobre el tipo de valor T — TypeScript infiere el tipo desde el prop value/defaultValue.
  Depende de @base-ui/react y lucide-react.

code:
  "use client"

  import * as React from "react"
  import { cn } from "../core/cn"
  import { ChevronDown } from "lucide-react"

  // ── Native select wrapper ─────────────────────────────────────────────────

  interface SelectProps extends React.ComponentPropsWithoutRef<"select"> {
    placeholder?: string
  }

  const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
    ({ className, children, placeholder, ...props }, ref) => {
      return (
        <div className="relative">
          <select
            ref={ref}
            data-slot="select"
            className={cn(
              "flex h-8 w-full appearance-none items-center rounded-lg border border-input bg-transparent px-2.5 pr-8 py-1 text-sm transition-colors outline-none",
              "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
              "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
              "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
              className,
            )}
            {...props}
          >
            {placeholder && (
              <option value="" disabled>
                {placeholder}
              </option>
            )}
            {children}
          </select>
          <ChevronDown className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
        </div>
      )
    },
  )
  Select.displayName = "Select"

  // ── Sub-components (thin wrappers for API compatibility) ──────────────────

  function SelectItem({
    className,
    ...props
  }: React.ComponentPropsWithoutRef<"option">) {
    return <option className={className} {...props} />
  }

  function SelectGroup({
    className,
    ...props
  }: React.ComponentPropsWithoutRef<"optgroup">) {
    return <optgroup className={className} {...props} />
  }

  function SelectGroupLabel(_props: { children?: React.ReactNode }) {
    // optgroup uses `label` attr, this is a no-op for compatibility
    return null
  }

  // Stubs for barrel export compatibility — these are no-ops with native select
  function SelectContent({ children }: { children?: React.ReactNode; className?: string }) {
    return <>{children}</>
  }
  function SelectTrigger({ children }: { children?: React.ReactNode; className?: string }) {
    return <>{children}</>
  }
  function SelectValue(_props: { placeholder?: string }) {
    return null
  }
  function SelectPortal({ children }: { children?: React.ReactNode }) {
    return <>{children}</>
  }
  function SelectSeparator({ className, ...props }: React.ComponentProps<"div">) {
    return <div className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
  }

  export {
    Select,
    SelectContent,
    SelectGroup,
    SelectGroupLabel,
    SelectItem,
    SelectPortal,
    SelectSeparator,
    SelectTrigger,
    SelectValue,
  }

params_schema:
  {"output":"Componente Select que renderiza dropdown genérico accesible con grupos, separadores y posicionamiento automático"}


id: settings_page_ts_ui
name: settings_page
kind: function
lang: ts
domain: ui
version: 1.0.0
purity: pure
signature: settingsPage(props: SettingsPageProps): ReactElement
tags: ["settings","page","form","sections","factory","composition","ui"]
uses_functions: ["cn_ts_core"]
uses_types: []
returns: []
imports: ["react"]
tests: []
file_path: frontend/functions/ui/settings_page.tsx
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T16:40:39Z
props: []
emits: []
variant: []
content_hash: 3b472b8211108cca344549bc2fd429125ed36860b2e7b2ed839620e0a1fc777f
source_repo: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library
source_license: MIT
description:
  Genera una página de configuración con navegación lateral, secciones y campos de formulario (text, number, toggle, select, textarea).

example:
  ```tsx
  settingsPage({
    title: 'Project Settings',
    sections: [
      {
        id: 'general',
        title: 'General',
        description: 'Basic project configuration',
        fields: [
          { key: 'name', label: 'Project Name', type: 'text', value: 'My Project' },
          { key: 'visibility', label: 'Public', description: 'Make project visible to everyone', type: 'toggle', value: true },
          { key: 'language', label: 'Language', type: 'select', options: [{ label: 'English', value: 'en' }, { label: 'Spanish', value: 'es' }] },
        ],
      },
      {
        id: 'notifications',
        title: 'Notifications',
        fields: [
          { key: 'email', label: 'Email notifications', type: 'toggle', value: false },
          { key: 'webhook', label: 'Webhook URL', type: 'text', placeholder: 'https://...' },
        ],
      },
    ],
    onSave: handleSave,
  })
  ```

notes:
  Layout de settings estándar con sidebar de navegación (oculta en mobile). Secciones con anclas para scroll. Soporta 5 tipos de campo.

code:
  import * as React from 'react'
  import { cn } from '../core/cn'

  interface SettingField {
    key: string
    label: string
    description?: string
    type: 'text' | 'number' | 'toggle' | 'select' | 'textarea'
    value?: unknown
    options?: Array<{ label: string; value: string }>
    placeholder?: string
  }

  interface SettingSection {
    id: string
    title: string
    description?: string
    fields: SettingField[]
  }

  interface SettingsPageProps {
    title?: string
    subtitle?: string
    sections: SettingSection[]
    onSave?: (values: Record<string, unknown>) => void
    className?: string
  }

  export function settingsPage({
    title = 'Settings',
    subtitle,
    sections,
    onSave,
    className,
  }: SettingsPageProps): React.ReactElement {
    return (
      <div className={cn('space-y-6', className)}>
        {/* Header */}
        <div className="border-b pb-4">
          <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
          {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
        </div>

        {/* Tabs navigation */}
        <div className="flex gap-6">
          <nav className="hidden w-48 shrink-0 md:block">
            <div className="space-y-1">
              {sections.map((section) => (
                <a
                  key={section.id}
                  href={`#${section.id}`}
                  className="block rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground"
                >
                  {section.title}
                </a>
              ))}
            </div>
          </nav>

          {/* Sections */}
          <div className="flex-1 space-y-8">
            {sections.map((section) => (
              <div key={section.id} id={section.id} className="space-y-4">
                <div>
                  <h2 className="text-lg font-medium">{section.title}</h2>
                  {section.description && <p className="text-sm text-muted-foreground">{section.description}</p>}
                </div>
                <div className="space-y-4 rounded-lg border p-4">
                  {section.fields.map((field) => (
                    <div key={field.key} className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:justify-between">
                      <div className="space-y-0.5">
                        <label className="text-sm font-medium">{field.label}</label>
                        {field.description && <p className="text-xs text-muted-foreground">{field.description}</p>}
                      </div>
                      <div className="w-full sm:w-64">
                        {field.type === 'toggle' ? (
                          <button className={cn(
                            'relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors',
                            field.value ? 'bg-primary' : 'bg-input'
                          )}>
                            <span className={cn('pointer-events-none block size-4 rounded-full bg-background shadow-lg ring-0 transition-transform', field.value ? 'translate-x-4' : 'translate-x-0')} />
                          </button>
                        ) : field.type === 'select' ? (
                          <select className="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm">
                            {field.options?.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
                          </select>
                        ) : field.type === 'textarea' ? (
                          <textarea className="w-full rounded-lg border border-input bg-transparent px-2.5 py-1.5 text-sm" rows={3} placeholder={field.placeholder} defaultValue={String(field.value ?? '')} />
                        ) : (
                          <input type={field.type} className="h-8 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm" placeholder={field.placeholder} defaultValue={String(field.value ?? '')} />
                        )}
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            ))}

            {onSave && (
              <div className="flex justify-end border-t pt-4">
                <button className="inline-flex h-8 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/80">
                  Save changes
                </button>
              </div>
            )}
          </div>
        </div>
      </div>
    )
  }

  export type { SettingsPageProps, SettingSection, SettingField }

params_schema:
  {"params":[{"name":"props","desc":"Configuración: título, secciones con campos (text, number, toggle, select, textarea) y callback onSave"}],"output":"Componente ReactElement que renderiza página de configuración con sidebar y secciones navegables"}


