merge: quick/mantine-cpp-new-functions — Mantine v9, C++, OSINT refactor, nuevas funciones

This commit is contained in:
2026-04-06 23:47:41 +02:00
282 changed files with 11283 additions and 6482 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ fn-registry/
python/functions/ # .py + .md por funcion Python
python/types/ # .py + .md por tipo Python
bash/functions/ # .sh + .md por funcion Bash (core, infra, io, shell)
frontend/ # pnpm + vite + react + tailwind + shadcn
frontend/ # pnpm + vite + react + mantine
frontend/functions/ # .tsx/.ts + .md (core para TS puro, ui para componentes React)
frontend/types/ # .ts + .md por tipo
registry/ # Paquete Go: modelos, SQLite, parser, indexer, validacion, migraciones
+114 -197
View File
@@ -2,6 +2,17 @@
Eres un arquitecto frontend experto. Esta skill se activa cuando el usuario pide crear un proyecto frontend, una app con UI, un componente nuevo, o una feature frontend. Tu trabajo es garantizar que TODO el frontend se construya usando el sistema de funciones reutilizables del registry y las mejores practicas actuales.
## Stack
- **pnpm** — gestor de paquetes
- **React 19** — UI library
- **Vite 8** — build tool
- **Mantine v9** — component library + styling (props, no CSS manual)
- **Phosphor Icons** — `@phosphor-icons/react`
- **Recharts** — charts (via `@mantine/charts`)
**NO usar:** Tailwind, shadcn, CVA, clsx, cn(), lucide-react, styled-components, emotion, CSS-in-JS runtime.
---
## PASO 1: Consultar el registry (OBLIGATORIO)
@@ -56,11 +67,12 @@ apps/{nombre}/
package.json
vite.config.ts
tsconfig.json
postcss.config.cjs
index.html
src/
main.tsx # Entry point
App.tsx # Root con ThemeProvider + Router
app.css # Tokens CSS — NUNCA hardcodear colores
main.tsx # Entry point con MantineProvider
App.tsx # Root con Router
app.css # Minimal (font-smoothing solo)
features/ # Feature-based co-location
{feature}/
components/ # Componentes del feature
@@ -87,21 +99,20 @@ apps/{nombre}/
"preview": "vite preview --host"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.577.0",
"@mantine/core": "^9.0.0",
"@mantine/hooks": "^9.0.0",
"@mantine/notifications": "^9.0.0",
"@phosphor-icons/react": "^2.1.10",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"recharts": "^2.15.0",
"tailwind-merge": "^3.5.0"
"react-dom": "^19.2.4"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.0",
"tailwindcss": "^4.2.2",
"postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"typescript": "~5.9.3",
"vite": "^8.0.0"
}
@@ -109,10 +120,10 @@ apps/{nombre}/
```
Agregar dependencias extras segun necesidad:
- **Charts**: `@mantine/charts`, `recharts`
- **Tablas**: `@tanstack/react-table`
- **Charts**: `recharts`
- **Iconos extra**: `@phosphor-icons/react`
- **Forms**: `react-hook-form`, `@hookform/resolvers`, `zod`
- **Dates**: `@mantine/dates`, `dayjs`
- **Router**: `react-router` o `@tanstack/react-router`
- **State**: `zustand` (client state), `@tanstack/react-query` (server state)
- **Wails**: los hooks de Wails ya estan en `@fn_library` (useWailsQuery, useWailsMutation, useWailsStream, useWailsEvent, WailsProvider)
@@ -122,11 +133,10 @@ Agregar dependencias extras segun necesidad:
```ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { resolve } from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
plugins: [react()],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
@@ -134,6 +144,9 @@ export default defineConfig({
},
dedupe: ['react', 'react-dom'],
},
css: {
postcss: resolve(__dirname, './postcss.config.cjs'),
},
build: {
target: 'es2022',
rollupOptions: {
@@ -147,108 +160,32 @@ export default defineConfig({
})
```
### postcss.config.cjs base
```js
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
```
### app.css base
```css
@import "tailwindcss";
@theme inline {
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, sans-serif;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
}
/* Dark theme (default) */
:root,
[data-theme="dark"] {
--background: oklch(8% 0.015 260);
--foreground: oklch(95% 0.01 260);
--muted: oklch(18% 0.02 260);
--muted-foreground: oklch(60% 0.02 260);
--border: oklch(15% 0.01 260);
--primary: oklch(65% 0.22 260);
--primary-foreground: oklch(98% 0.01 260);
--secondary: oklch(20% 0.02 260);
--secondary-foreground: oklch(95% 0.01 260);
--accent: oklch(18% 0.03 260);
--accent-foreground: oklch(95% 0.01 260);
--destructive: oklch(55% 0.22 25);
--destructive-foreground: oklch(98% 0.01 260);
--card: oklch(11% 0.015 260);
--card-foreground: oklch(95% 0.01 260);
--popover: oklch(12% 0.015 260);
--popover-foreground: oklch(95% 0.01 260);
--ring: oklch(65% 0.22 260);
--input: oklch(22% 0.02 260);
--radius: 0.5rem;
--chart-1: oklch(62% 0.19 260);
--chart-2: oklch(65% 0.2 155);
--chart-3: oklch(75% 0.18 85);
--chart-4: oklch(60% 0.22 25);
--chart-5: oklch(60% 0.2 300);
}
/* Light theme */
[data-theme="light"] {
--background: oklch(99% 0.005 260);
--foreground: oklch(15% 0.01 260);
--muted: oklch(95% 0.01 260);
--muted-foreground: oklch(45% 0.02 260);
--border: oklch(90% 0.01 260);
--primary: oklch(50% 0.22 260);
--primary-foreground: oklch(98% 0.01 260);
--secondary: oklch(95% 0.01 260);
--secondary-foreground: oklch(20% 0.01 260);
--accent: oklch(95% 0.02 260);
--accent-foreground: oklch(20% 0.01 260);
--destructive: oklch(55% 0.22 25);
--destructive-foreground: oklch(98% 0.01 260);
--card: oklch(100% 0 0);
--card-foreground: oklch(15% 0.01 260);
--popover: oklch(100% 0 0);
--popover-foreground: oklch(15% 0.01 260);
--ring: oklch(50% 0.22 260);
--input: oklch(90% 0.01 260);
--radius: 0.5rem;
--chart-1: oklch(55% 0.22 260);
--chart-2: oklch(55% 0.2 155);
--chart-3: oklch(65% 0.18 85);
--chart-4: oklch(55% 0.22 25);
--chart-5: oklch(55% 0.2 300);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
/* Minimal — Mantine handles all theming via MantineProvider */
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (prefers-reduced-motion: reduce) {
@@ -259,18 +196,33 @@ export default defineConfig({
}
```
### App.tsx base
### main.tsx base
```tsx
import { ThemeProvider } from '@fn_library'
import '@mantine/core/styles.css'
import '@mantine/notifications/styles.css'
import './app.css'
export default function App() {
return (
<ThemeProvider defaultTheme="dark">
{/* Router y contenido aqui */}
</ThemeProvider>
)
}
import React from 'react'
import ReactDOM from 'react-dom/client'
import { MantineProvider, createTheme } from '@mantine/core'
import { Notifications } from '@mantine/notifications'
import App from './App'
const theme = createTheme({
primaryColor: 'blue',
defaultRadius: 'md',
// Customize colors, fonts, etc. here
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MantineProvider theme={theme} defaultColorScheme="dark">
<Notifications />
<App />
</MantineProvider>
</React.StrictMode>,
)
```
### Despues del scaffold
@@ -287,17 +239,16 @@ Para componentes nuevos que van al registry en `frontend/functions/`.
### Reglas de implementacion
1. **Headless first**: usar `@base-ui/react` como primitivo si el componente es interactivo (dialog, select, tooltip, etc.)
2. **CVA para variantes**: SIEMPRE usar `class-variance-authority` para definir variantes
3. **cn() para clases**: SIEMPRE usar `cn()` de `frontend/functions/core/cn.ts` para componer classNames
4. **CSS variables**: NUNCA hex/rgb/oklch inline en el componente — solo clases Tailwind que mapean a CSS variables (`bg-primary`, `text-muted-foreground`, `border-border`)
5. **Props tipadas**: usar `React.ComponentPropsWithoutRef<"element">` para HTML props spreading
1. **Mantine first**: wrappear componentes de Mantine. Solo crear desde cero si Mantine no tiene equivalente.
2. **Styling via props**: usar props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, `gap`, etc.) y el style system. NUNCA clases CSS manuales ni Tailwind.
3. **CSS variables de Mantine**: si necesitas styles inline, usar `var(--mantine-color-*)`, `var(--mantine-spacing-*)`, etc.
4. **Iconos**: usar `@phosphor-icons/react`, no lucide-react ni @tabler/icons-react.
5. **Props tipadas**: usar `React.ComponentPropsWithoutRef<"element">` para HTML props spreading.
6. **Accesibilidad**:
- Elementos semanticos: `<button>` para acciones, `<a>` para navegacion, `<dialog>` para modales
- Elementos semanticos: `<button>` para acciones, `<a>` para navegacion
- NUNCA `<div onClick>` para elementos interactivos
- `aria-label` o `aria-labelledby` en todo componente interactivo
- `aria-label` en botones de solo icono
- `aria-invalid` + `aria-describedby` en inputs con error
- `role="status"` para loading states
- Focus management en modales/popovers
7. **Discriminated unions** cuando las props cambian segun variante:
@@ -311,54 +262,19 @@ type Props = { size?: 'sm' | 'md' | 'lg'; children: React.ReactNode } & (
### Patron de archivo .tsx
```tsx
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../core/cn'
import { Select, type SelectProps } from '@mantine/core'
const componentVariants = cva(
'base-classes-here', // clases base
{
variants: {
variant: {
default: 'classes...',
secondary: 'classes...',
},
size: {
sm: 'classes...',
md: 'classes...',
lg: 'classes...',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
}
)
interface ComponentProps
extends React.ComponentPropsWithoutRef<'div'>,
VariantProps<typeof componentVariants> {
// props adicionales con JSDoc
/** Descripcion de la prop */
// Re-export con defaults o logica adicional si necesario
interface MySelectProps extends Omit<SelectProps, 'xxx'> {
customProp?: string
}
const Component = React.forwardRef<HTMLDivElement, ComponentProps>(
({ className, variant, size, customProp, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(componentVariants({ variant, size }), className)}
{...props}
/>
)
}
)
Component.displayName = 'Component'
function MySelect({ customProp, ...props }: MySelectProps) {
return <Select {...props} />
}
export { Component, componentVariants }
export type { ComponentProps }
export { MySelect }
export type { MySelectProps }
```
### Patron de archivo .md
@@ -376,12 +292,12 @@ purity: impure
signature: "ComponentName(props: ComponentProps): JSX.Element"
description: "Descripcion concisa de que hace el componente"
tags: [component, ui, ...]
uses_functions: [cn_ts_core]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", "class-variance-authority"]
imports: ["@mantine/core"]
tested: false
tests: []
test_file_path: ""
@@ -391,14 +307,10 @@ props:
type: "'default' | 'secondary'"
required: false
description: "Estilo visual"
- name: className
type: "string"
required: false
description: "Clases CSS adicionales"
emits: []
has_state: false
framework: react
variant: [default, secondary]
variant: [default]
---
## Ejemplo
@@ -493,7 +405,7 @@ function useFeatureData() {
```tsx
import { lazy, Suspense } from 'react'
import { Skeleton } from '@fn_library'
import { Skeleton } from '@mantine/core'
const FeaturePage = lazy(() => import('./features/feature/components/FeaturePage'))
@@ -501,7 +413,7 @@ function AppRoutes() {
return (
<Routes>
<Route path="/feature" element={
<Suspense fallback={<Skeleton className="h-screen w-full" />}>
<Suspense fallback={<Skeleton height="100vh" />}>
<FeaturePage />
</Suspense>
} />
@@ -517,14 +429,19 @@ function AppRoutes() {
Antes de dar por terminado cualquier trabajo frontend, verificar:
### Colores y estilos
- [ ] CERO colores hardcodeados (no hex, no rgb, no oklch inline en componentes)
- [ ] Solo clases Tailwind mapeadas a CSS variables: `bg-primary`, `text-foreground`, `border-border`, etc.
- [ ] `cn()` usado para merge de clases en todo componente
- [ ] CVA usado para variantes (no condicionales manuales con ternarios)
- [ ] CERO colores hardcodeados en componentes (no hex, no rgb inline)
- [ ] Styling via props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, `gap`, etc.)
- [ ] Si se necesitan styles inline, usar CSS variables de Mantine (`var(--mantine-color-*)`)
- [ ] NO clases CSS manuales, NO Tailwind, NO cn(), NO CVA
### Componentes del registry
- [ ] Verificado que no se esta recreando algo que ya existe en `@fn_library` (`frontend/functions/ui/`)
- [ ] Componentes de `@fn_library` usados donde aplica: Alert, Badge, Button, Card, Dialog, Input, Label, Select, SimpleSelect, Skeleton, Sparkline, Tabs, Tooltip, FormField, PageHeader, ProgressBar, KPICard, ThemeProvider, DashboardLayout, DataTable, charts (AreaChart, BarChart, LineChart, PieChart, ChartContainer), hooks Wails (useWailsQuery, useWailsMutation, useWailsStream, useWailsEvent)
- [ ] Componentes de `@fn_library` usados donde aplica: Card, Select, SimpleSelect, KPICard, Sparkline, DashboardLayout, DataTable, charts, hooks Wails
- [ ] Componentes de Mantine usados directamente donde `@fn_library` no tiene wrapper: Button, TextInput, Table, Alert, Badge, Skeleton, Tabs, Tooltip, Group, Stack, Grid, Box, Paper, AppShell, Container
### Iconos
- [ ] Usando `@phosphor-icons/react` para iconos
- [ ] NO lucide-react, NO @tabler/icons-react
### TypeScript
- [ ] Props interfaces con `React.ComponentPropsWithoutRef` para HTML spreading
@@ -533,7 +450,7 @@ Antes de dar por terminado cualquier trabajo frontend, verificar:
- [ ] No `any` — usar `unknown` + type guards si es necesario
### Accesibilidad
- [ ] Elementos semanticos (button, a, dialog — no div onClick)
- [ ] Elementos semanticos (button, a — no div onClick)
- [ ] `aria-label` en botones de solo icono
- [ ] `aria-invalid` + `aria-describedby` en inputs con validacion
- [ ] Focus trap en modales y popovers
@@ -555,15 +472,15 @@ Antes de dar por terminado cualquier trabajo frontend, verificar:
## ANTI-PATRONES (nunca hacer)
1. **`<div onClick={...}>`** → usar `<button>` o Base-UI primitivo
2. **`style={{ color: '#3b82f6' }}`** → usar `className="text-primary"`
3. **`import Button from './MyButton'`** cuando existe en la lib → usar `import { Button } from '@fn_library'`
1. **`<div onClick={...}>`** → usar `<button>` o componente Mantine
2. **`style={{ color: '#3b82f6' }}`** → usar prop `c="blue"` o `var(--mantine-color-blue-6)`
3. **`import Button from './MyButton'`** cuando existe en Mantine → usar `import { Button } from '@mantine/core'`
4. **Estado global para todo** → segmentar: server state (React Query), client state (Zustand), form state (React Hook Form), URL state (search params)
5. **`index.ts` en la raiz de `src/`** que re-exporta todo → mata tree-shaking
6. **`// @ts-ignore`** → arreglar el tipo
7. **CSS-in-JS runtime** (styled-components, emotion) → usar Tailwind
8. **Instalar shadcn/ui como dependencia** → los componentes ya estan en el registry, usar `@fn_library`
9. **Crear utilidades que ya existen**: `cn()`, `getSeriesColor()`, `ChartContainer`, `ThemeProvider` ya estan en `@fn_library`
10. **Colores de chart hardcodeados** → usar `--chart-1` a `--chart-5` o `getSeriesColor()`
7. **CSS-in-JS runtime** (styled-components, emotion) → usar props de Mantine
8. **Tailwind, CVA, cn(), clsx** → usar props de Mantine y su style system
9. **Crear utilidades que ya existen**: `getSeriesColor()`, `ChartContainer`, `DashboardLayout`, `DataTable` ya estan en `@fn_library`
10. **Colores de chart hardcodeados** → usar `@mantine/charts` color system o `getSeriesColor()`
$ARGUMENTS
+9 -1
View File
@@ -1,3 +1,11 @@
En todos los frontends se usan los componentes de `@fn_library` (alias a `frontend/functions/ui/`) antes que elementos HTML nativos o librerias externas.
En todos los frontends se usa el sistema de temas basado en CSS variables (`--background`, `--foreground`, `--input`, `--border`, `--popover`, etc.) definidas en `app.css`. Los componentes deben leer estas variables para adaptarse al tema activo. Nunca hardcodear colores.
El sistema de UI es Mantine v9. Todos los componentes de @fn_library wrappean componentes de Mantine.
**Theming:** Cada app define su tema con `createTheme()` de `@mantine/core` y lo pasa a `MantineProvider` (o `FnMantineProvider` de @fn_library). No se usan CSS variables custom — Mantine genera las suyas automaticamente (`--mantine-color-*`).
**Styling:** No se usa Tailwind, CVA, cn(), ni clases CSS manuales. Los componentes se estilizan con props de Mantine (`size`, `color`, `variant`, `p`, `m`, `fw`, etc.) y el style system de Mantine.
**Iconos:** Se usa `@tabler/icons-react` (el set nativo de Mantine), no lucide-react.
**Layout:** Se usan los componentes de layout de Mantine: `Group`, `Stack`, `Grid`, `Flex`, `SimpleGrid`, `AppShell`, `Container`, `Box`, `Paper`.
+3
View File
@@ -43,6 +43,9 @@ analysis/*/
# Sources — repos externos clonados (solo se versiona el manifest)
sources/*/
# C++ build artifacts
cpp/build/
# OS
.DS_Store
Thumbs.db
+13
View File
@@ -0,0 +1,13 @@
[submodule "cpp/vendor/imgui"]
path = cpp/vendor/imgui
url = https://github.com/ocornut/imgui.git
branch = docking
[submodule "cpp/vendor/implot"]
path = cpp/vendor/implot
url = https://github.com/epezent/implot.git
[submodule "cpp/vendor/tracy"]
path = cpp/vendor/tracy
url = https://github.com/wolfpld/tracy.git
[submodule "/home/lucas/fn_registry/cpp/vendor/glfw"]
path = /home/lucas/fn_registry/cpp/vendor/glfw
url = https://github.com/glfw/glfw.git
+36
View File
@@ -0,0 +1,36 @@
---
name: build_cpp_linux
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "build_cpp_linux(target?: string) -> void"
description: "Compila las funciones y apps C++ del registry para Linux nativo usando cmake"
tags: [cpp, build, cmake, linux, imgui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/build_cpp_linux.sh"
params:
- name: target
desc: "Nombre del target cmake a compilar (opcional, sin argumento compila todo)"
output: "Compila los binarios en cpp/build/linux/"
---
# build_cpp_linux
Configura y compila el proyecto C++ (ImGui/ImPlot) para Linux nativo.
Usa cmake con compilacion paralela (`-j$(nproc)`). Si no se ha configurado antes, ejecuta `cmake -B` automaticamente.
```bash
fn run build_cpp_linux # Compilar todo
fn run build_cpp_linux chart_demo # Compilar solo chart_demo
```
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
REGISTRY_ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"
CPP_ROOT="$REGISTRY_ROOT/cpp"
BUILD_DIR="$CPP_ROOT/build/linux"
TARGET="${1:-}"
# Configure if needed
if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
echo "[build_cpp_linux] Configuring cmake..."
cmake -B "$BUILD_DIR" -S "$CPP_ROOT"
fi
# Build
if [ -n "$TARGET" ]; then
echo "[build_cpp_linux] Building target: $TARGET"
cmake --build "$BUILD_DIR" --target "$TARGET" -- -j"$(nproc)"
else
echo "[build_cpp_linux] Building all targets..."
cmake --build "$BUILD_DIR" -- -j"$(nproc)"
fi
echo "[build_cpp_linux] Done. Binaries in $BUILD_DIR"
+38
View File
@@ -0,0 +1,38 @@
---
name: build_cpp_windows
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "build_cpp_windows(target?: string) -> void"
description: "Cross-compila las funciones y apps C++ del registry para Windows usando mingw-w64"
tags: [cpp, build, cmake, windows, cross-compile, mingw, imgui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/build_cpp_windows.sh"
params:
- name: target
desc: "Nombre del target cmake a compilar (opcional, sin argumento compila todo)"
output: "Produce binarios .exe de Windows en cpp/build/windows/"
---
# build_cpp_windows
Cross-compila el proyecto C++ para Windows desde Linux usando el toolchain mingw-w64.
Los .exe resultantes incluyen runtime linkado estaticamente (self-contained).
```bash
fn run build_cpp_windows # Compilar todo
fn run build_cpp_windows chart_demo # Compilar solo chart_demo
```
Requiere `mingw-w64`: `sudo apt install mingw-w64`
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
REGISTRY_ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../.." && pwd)}"
CPP_ROOT="$REGISTRY_ROOT/cpp"
BUILD_DIR="$CPP_ROOT/build/windows"
TOOLCHAIN="$CPP_ROOT/toolchains/mingw-w64.cmake"
TARGET="${1:-}"
# Check mingw is available
if ! command -v x86_64-w64-mingw32-g++ &>/dev/null; then
echo "[build_cpp_windows] Error: mingw-w64 not found. Install with: sudo apt install mingw-w64"
exit 1
fi
# Configure if needed
if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
echo "[build_cpp_windows] Configuring cmake with mingw-w64 toolchain..."
cmake -B "$BUILD_DIR" -S "$CPP_ROOT" -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN"
fi
# Build
if [ -n "$TARGET" ]; then
echo "[build_cpp_windows] Cross-compiling target: $TARGET"
cmake --build "$BUILD_DIR" --target "$TARGET" -- -j"$(nproc)"
else
echo "[build_cpp_windows] Cross-compiling all targets..."
cmake --build "$BUILD_DIR" -- -j"$(nproc)"
fi
echo "[build_cpp_windows] Done. Windows binaries in $BUILD_DIR"
if [ -n "$TARGET" ]; then
file "$BUILD_DIR"/**/"$TARGET".exe 2>/dev/null || file "$BUILD_DIR/$TARGET".exe 2>/dev/null || true
fi
+54
View File
@@ -0,0 +1,54 @@
---
name: frontend_doctor
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "frontend_doctor(project_dir: string) -> diagnostics_stdout"
description: "Diagnostica la salud de un proyecto frontend Mantine. Verifica Node, React, Mantine, PostCSS, TypeScript, vite.config y detecta residuos de shadcn/@base-ui. Imprime tabla de checks con exit code 0/1."
tags: [frontend, mantine, doctor, diagnostics, health, validation]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: project_dir
desc: "directorio del proyecto frontend con package.json"
output: "tabla de checks con ✓/✗ por cada validación y resumen final"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/frontend_doctor.sh"
---
## Ejemplo
```bash
# Diagnosticar un proyecto
bash frontend_doctor.sh ./apps/rapid_dashboards/frontend
# Output:
# === Frontend Doctor: ./apps/rapid_dashboards/frontend ===
#
# ✓ Node >= 18 22.12.0
# ✓ Package manager detected pnpm
# ✓ node_modules present
# ✓ @mantine/core 7.17.0
# ✓ @mantine/hooks
# ✓ @mantine/charts
# ✓ React >= 18 19.2.4
# ✓ postcss.config present
# ✓ TypeScript >= 5 6.0.2
# ✓ vite.config present
# ✓ No shadcn residual
# ✓ No @base-ui residual
#
# Resultado: todo OK
```
## Notas
Checks informativos, no modifica nada. Util para validar que un proyecto esta correctamente configurado despues de instalar Mantine o migrar desde shadcn. Exit code 0 si todo OK, 1 si hay problemas.
+150
View File
@@ -0,0 +1,150 @@
# frontend_doctor
# ----------------
# Diagnostica la salud de un proyecto frontend Mantine.
# Verifica dependencias, configuracion y versiones.
# Imprime tabla de checks y retorna exit code 0 (ok) o 1 (fallos).
#
# USO (sourced):
# source frontend_doctor.sh
# frontend_doctor /path/to/frontend
#
# USO (directo):
# bash frontend_doctor.sh /path/to/frontend
frontend_doctor() {
local project_dir="$1"
local failures=0
if [ -z "$project_dir" ]; then
echo "frontend_doctor: se requiere project_dir" >&2
return 1
fi
if [ ! -f "$project_dir/package.json" ]; then
echo "frontend_doctor: no existe package.json en $project_dir" >&2
return 1
fi
echo "=== Frontend Doctor: $project_dir ==="
echo ""
# Helper: check y reportar
_check() {
local label="$1"
local ok="$2"
local detail="$3"
if [ "$ok" = "1" ]; then
printf " ✓ %-35s %s\n" "$label" "$detail"
else
printf " ✗ %-35s %s\n" "$label" "$detail"
((failures++))
fi
}
# 1. Node >= 18
local node_ver=""
local node_ok=0
if command -v node &>/dev/null; then
node_ver=$(node -v 2>/dev/null | sed 's/v//')
local node_major=$(echo "$node_ver" | cut -d. -f1)
[ "$node_major" -ge 18 ] 2>/dev/null && node_ok=1
fi
_check "Node >= 18" "$node_ok" "${node_ver:-not found}"
# 2. Package manager
local pm_ok=0
local pm_name="none"
if [ -f "$project_dir/pnpm-lock.yaml" ]; then
pm_name="pnpm"; pm_ok=1
elif [ -f "$project_dir/yarn.lock" ]; then
pm_name="yarn"; pm_ok=1
elif [ -f "$project_dir/package-lock.json" ]; then
pm_name="npm"; pm_ok=1
fi
_check "Package manager detected" "$pm_ok" "$pm_name"
# 3. node_modules existe
local nm_ok=0
[ -d "$project_dir/node_modules" ] && nm_ok=1
_check "node_modules present" "$nm_ok" ""
# 4. @mantine/core instalado
local mantine_ok=0
local mantine_ver=""
if [ -f "$project_dir/node_modules/@mantine/core/package.json" ]; then
mantine_ver=$(node -e "console.log(require('$project_dir/node_modules/@mantine/core/package.json').version)" 2>/dev/null)
mantine_ok=1
fi
_check "@mantine/core" "$mantine_ok" "${mantine_ver:-not installed}"
# 5. @mantine/hooks
local hooks_ok=0
[ -d "$project_dir/node_modules/@mantine/hooks" ] && hooks_ok=1
_check "@mantine/hooks" "$hooks_ok" ""
# 6. @mantine/charts
local charts_ok=0
[ -d "$project_dir/node_modules/@mantine/charts" ] && charts_ok=1
_check "@mantine/charts" "$charts_ok" ""
# 7. React >= 18
local react_ok=0
local react_ver=""
if [ -f "$project_dir/node_modules/react/package.json" ]; then
react_ver=$(node -e "console.log(require('$project_dir/node_modules/react/package.json').version)" 2>/dev/null)
local react_major=$(echo "$react_ver" | cut -d. -f1)
[ "$react_major" -ge 18 ] 2>/dev/null && react_ok=1
fi
_check "React >= 18" "$react_ok" "${react_ver:-not found}"
# 8. postcss.config presente
local postcss_ok=0
if [ -f "$project_dir/postcss.config.cjs" ] || [ -f "$project_dir/postcss.config.js" ] || [ -f "$project_dir/postcss.config.mjs" ]; then
postcss_ok=1
fi
_check "postcss.config present" "$postcss_ok" ""
# 9. TypeScript >= 5
local ts_ok=0
local ts_ver=""
if [ -f "$project_dir/node_modules/typescript/package.json" ]; then
ts_ver=$(node -e "console.log(require('$project_dir/node_modules/typescript/package.json').version)" 2>/dev/null)
local ts_major=$(echo "$ts_ver" | cut -d. -f1)
[ "$ts_major" -ge 5 ] 2>/dev/null && ts_ok=1
fi
_check "TypeScript >= 5" "$ts_ok" "${ts_ver:-not found}"
# 10. vite.config presente
local vite_ok=0
if [ -f "$project_dir/vite.config.ts" ] || [ -f "$project_dir/vite.config.js" ]; then
vite_ok=1
fi
_check "vite.config present" "$vite_ok" ""
# 11. Shadcn residual (warning)
local shadcn_clean=1
if [ -f "$project_dir/components.json" ]; then
shadcn_clean=0
fi
_check "No shadcn residual" "$shadcn_clean" "$([ "$shadcn_clean" = "0" ] && echo 'components.json found')"
# 12. @base-ui residual (warning)
local baseui_clean=1
if [ -d "$project_dir/node_modules/@base-ui" ]; then
baseui_clean=0
fi
_check "No @base-ui residual" "$baseui_clean" "$([ "$baseui_clean" = "0" ] && echo '@base-ui still installed')"
echo ""
if [ "$failures" -eq 0 ]; then
echo " Resultado: todo OK"
return 0
else
echo " Resultado: $failures problema(s) encontrado(s)"
return 1
fi
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
frontend_doctor "$@"
fi
@@ -0,0 +1,53 @@
---
name: gitea_add_collaborator
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "gitea_add_collaborator(owner: string, repo: string, username: string, permission: string) -> void"
description: "Añade un colaborador a un repositorio Gitea con el nivel de permisos indicado. Silencioso si el colaborador ya existe (422)."
tags: [gitea, git, collaborator, permission, repo, api, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: owner
desc: "usuario u organización propietaria del repositorio"
- name: repo
desc: "nombre del repositorio"
- name: username
desc: "nombre de usuario del colaborador a añadir"
- name: permission
desc: "nivel de permisos: 'read', 'write' o 'admin' (default: admin)"
output: "vacío — efectos observables a través de la API de Gitea"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gitea_add_collaborator.sh"
---
## Ejemplo
```bash
source bash/functions/infra/gitea_add_collaborator.sh
export GITEA_URL="https://git.example.com"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Añadir colaborador con permiso admin (default)
gitea_add_collaborator "myorg" "my-app" "egutierrez"
# Añadir colaborador con permiso de solo lectura
gitea_add_collaborator "myorg" "my-app" "reviewer" "read"
```
## Notas
- Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
- Un 422 de la API indica que el usuario ya es colaborador — se trata como éxito silencioso.
- La función no produce salida a stdout; los mensajes informativos van a stderr.
- Nivel `admin` da acceso completo al repo incluyendo settings.
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# gitea_add_collaborator — Añade un colaborador a un repositorio Gitea
gitea_add_collaborator() {
local owner="$1"
local repo="$2"
local username="$3"
local permission="${4:-admin}"
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_add_collaborator: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_add_collaborator: GITEA_TOKEN no está seteado" >&2
return 1
fi
if [[ -z "$owner" || -z "$repo" || -z "$username" ]]; then
echo "gitea_add_collaborator: se requieren owner, repo y username" >&2
return 1
fi
local payload
payload=$(printf '{"permission":"%s"}' "$permission")
echo "gitea_add_collaborator: añadiendo '$username' a '$owner/$repo' con permiso '$permission'..." >&2
local response http_code
response=$(curl -s -w "\n%{http_code}" \
-X PUT \
-H "Content-Type: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
-d "$payload" \
"${GITEA_URL}/api/v1/repos/${owner}/${repo}/collaborators/${username}")
http_code=$(echo "$response" | tail -n1)
local body
body=$(echo "$response" | head -n -1)
if [[ "$http_code" == "204" || "$http_code" == "200" ]]; then
echo "gitea_add_collaborator: '$username' añadido a '$owner/$repo'" >&2
return 0
fi
if [[ "$http_code" == "422" ]]; then
echo "gitea_add_collaborator: '$username' ya es colaborador de '$owner/$repo' (silencioso)" >&2
return 0
fi
echo "gitea_add_collaborator: error (HTTP ${http_code}): ${body}" >&2
return 1
}
+56
View File
@@ -0,0 +1,56 @@
---
name: gitea_create_repo
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "gitea_create_repo(owner: string, name: string, private: string, description: string) -> string"
description: "Crea un repositorio en Gitea para un owner. Intenta crearlo en org primero; si el owner no es una org (404/422), lo crea en el usuario autenticado. No falla fatalmente si el repo ya existe (409)."
tags: [gitea, git, repo, create, infra, api]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: owner
desc: "usuario u organización propietaria del repo"
- name: name
desc: "nombre del repositorio a crear"
- name: private
desc: "si el repo es privado, 'true' o 'false' (default: false)"
- name: description
desc: "descripción del repositorio (opcional)"
output: "JSON del repositorio creado según la API de Gitea"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gitea_create_repo.sh"
---
## Ejemplo
```bash
source bash/functions/infra/gitea_create_repo.sh
export GITEA_URL="https://git.example.com"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Crear repo público en org o usuario
repo_json=$(gitea_create_repo "myorg" "my-app")
# Crear repo privado con descripción
repo_json=$(gitea_create_repo "myorg" "my-app" "true" "Mi aplicación principal")
# Extraer la URL del clon
clone_url=$(echo "$repo_json" | jq -r '.clone_url')
```
## Notas
- Requiere variables de entorno `GITEA_URL` y `GITEA_TOKEN` seteadas antes de invocar.
- El fallback org → usuario ocurre con HTTP 404 o 422 en el endpoint de orgs.
- Un 409 se reporta a stderr pero la función retorna 0 — el repo ya existe es una condición aceptable para idempotencia.
- Los mensajes informativos van a stderr; el JSON de respuesta va a stdout.
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# gitea_create_repo — Crea un repositorio en Gitea para un owner (org o usuario)
gitea_create_repo() {
local owner="$1"
local name="$2"
local private="${3:-false}"
local description="${4:-}"
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_create_repo: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_create_repo: GITEA_TOKEN no está seteado" >&2
return 1
fi
if [[ -z "$owner" || -z "$name" ]]; then
echo "gitea_create_repo: se requieren owner y name" >&2
return 1
fi
local payload
payload=$(printf '{"name":"%s","private":%s,"description":"%s","auto_init":false}' \
"$name" "$private" "$description")
echo "gitea_create_repo: intentando crear '$owner/$name' en org..." >&2
local response http_code
response=$(curl -s -w "\n%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
-d "$payload" \
"${GITEA_URL}/api/v1/orgs/${owner}/repos")
http_code=$(echo "$response" | tail -n1)
local body
body=$(echo "$response" | head -n -1)
if [[ "$http_code" == "201" ]]; then
echo "gitea_create_repo: repo '$owner/$name' creado en org" >&2
echo "$body"
return 0
fi
if [[ "$http_code" == "409" ]]; then
echo "gitea_create_repo: repo '$owner/$name' ya existe (409)" >&2
echo "$body"
return 0
fi
if [[ "$http_code" == "404" || "$http_code" == "422" ]]; then
echo "gitea_create_repo: org no encontrada (${http_code}), intentando en usuario..." >&2
response=$(curl -s -w "\n%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${GITEA_TOKEN}" \
-d "$payload" \
"${GITEA_URL}/api/v1/user/repos")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n -1)
if [[ "$http_code" == "201" ]]; then
echo "gitea_create_repo: repo '$owner/$name' creado en usuario" >&2
echo "$body"
return 0
fi
if [[ "$http_code" == "409" ]]; then
echo "gitea_create_repo: repo '$owner/$name' ya existe (409)" >&2
echo "$body"
return 0
fi
echo "gitea_create_repo: error al crear en usuario (HTTP ${http_code}): ${body}" >&2
return 1
fi
echo "gitea_create_repo: error inesperado (HTTP ${http_code}): ${body}" >&2
return 1
}
+51
View File
@@ -0,0 +1,51 @@
---
name: gitea_list_repos
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "gitea_list_repos(owner: string) -> string"
description: "Lista repositorios de un owner en Gitea. Intenta listar como org primero; si falla, lista como usuario. Imprime una línea por repo en formato name<TAB>html_url<TAB>description."
tags: [gitea, git, repo, list, org, user, api, infra]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: owner
desc: "nombre del usuario u organización cuyos repos se listan"
output: "una línea por repositorio con columnas separadas por tabulador: name, html_url, description"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gitea_list_repos.sh"
---
## Ejemplo
```bash
source bash/functions/infra/gitea_list_repos.sh
export GITEA_URL="https://git.example.com"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Listar todos los repos de una org
gitea_list_repos "myorg"
# my-app https://git.example.com/myorg/my-app Mi aplicación principal
# infra https://git.example.com/myorg/infra
# Iterar sobre los repos
while IFS=$'\t' read -r name url desc; do
echo "Repo: $name$url"
done < <(gitea_list_repos "myorg")
```
## Notas
- Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
- Usa `jq` si está disponible; fallback con grep/sed en caso contrario.
- El límite es 50 repos por página. Para owners con más de 50 repos habría que implementar paginación.
- Los mensajes informativos van a stderr; los datos tabulados van a stdout.
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# gitea_list_repos — Lista repositorios de un owner (org o usuario) en Gitea
gitea_list_repos() {
local owner="$1"
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_list_repos: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_list_repos: GITEA_TOKEN no está seteado" >&2
return 1
fi
if [[ -z "$owner" ]]; then
echo "gitea_list_repos: se requiere owner" >&2
return 1
fi
echo "gitea_list_repos: listando repos de '$owner' (intentando org)..." >&2
local response http_code body
response=$(curl -s -w "\n%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/orgs/${owner}/repos?limit=50")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n -1)
if [[ "$http_code" != "200" ]]; then
echo "gitea_list_repos: org no encontrada (${http_code}), intentando usuario..." >&2
response=$(curl -s -w "\n%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/users/${owner}/repos?limit=50")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n -1)
if [[ "$http_code" != "200" ]]; then
echo "gitea_list_repos: error listando repos de usuario (HTTP ${http_code}): ${body}" >&2
return 1
fi
fi
# Formatear salida como name\thtml_url\tdescription
if command -v jq &>/dev/null; then
echo "$body" | jq -r '.[] | [.name, .html_url, (.description // "")] | @tsv'
else
# Fallback sin jq: extraer campos básicos con grep/sed
echo "$body" | grep -o '"name":"[^"]*"\|"html_url":"[^"]*"\|"description":"[^"]*"' \
| paste - - - | sed 's/"name":"//;s/"html_url":"//;s/"description":"//;s/"//g'
fi
}
@@ -0,0 +1,55 @@
---
name: gitea_push_directory
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "gitea_push_directory(directory: string, owner: string, repo: string, branch: string) -> void"
description: "Inicializa git en un directorio local y lo sube a un repositorio Gitea existente. Si el directorio ya tiene .git, actualiza el remote y pushea cambios pendientes. Protege registry.db añadiéndolo al .gitignore antes del commit."
tags: [gitea, git, push, directory, sync, infra, repo]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: directory
desc: "ruta absoluta o relativa al directorio local a subir"
- name: owner
desc: "usuario u organización propietaria del repositorio Gitea destino"
- name: repo
desc: "nombre del repositorio Gitea destino (debe existir previamente)"
- name: branch
desc: "rama en la que hacer push (default: main)"
output: "vacío — efectos observables en el repositorio Gitea destino"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/gitea_push_directory.sh"
---
## Ejemplo
```bash
source bash/functions/infra/gitea_push_directory.sh
export GITEA_URL="https://git.example.com"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Subir directorio a repo existente
gitea_push_directory "/home/lucas/myproject" "myorg" "my-app"
# Subir a rama específica
gitea_push_directory "/home/lucas/myproject" "myorg" "my-app" "develop"
```
## Notas
- Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
- El token se embebe en la URL del remote para autenticación (nunca se imprime a stdout/stderr, se enmascara con ***).
- Si `registry.db` existe en el directorio, se añade automáticamente al `.gitignore` local.
- Si el `.git` ya existe con un remote diferente, se redirige al repo indicado sin perder el historial local.
- Usa `--force-with-lease` para el primer push y fallback a push normal (para repos vacíos recién creados).
- El commit se firma con `agent@fn-registry` si no hay configuración git en el entorno.
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# gitea_push_directory — Inicializa git en un directorio y lo sube a un repo Gitea existente
gitea_push_directory() {
local directory="$1"
local owner="$2"
local repo="$3"
local branch="${4:-main}"
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_push_directory: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_push_directory: GITEA_TOKEN no está seteado" >&2
return 1
fi
if [[ -z "$directory" || -z "$owner" || -z "$repo" ]]; then
echo "gitea_push_directory: se requieren directory, owner y repo" >&2
return 1
fi
if [[ ! -d "$directory" ]]; then
echo "gitea_push_directory: directorio '$directory' no existe" >&2
return 1
fi
# Construir URL con credenciales embebidas para autenticación
local gitea_host
gitea_host=$(echo "$GITEA_URL" | sed 's|https\?://||')
local remote_url="https://${GITEA_TOKEN}@${gitea_host}/${owner}/${repo}.git"
local display_url="https://***@${gitea_host}/${owner}/${repo}.git"
echo "gitea_push_directory: procesando '$directory' → '$owner/$repo' (rama: $branch)..." >&2
# Añadir registry.db al .gitignore local si existe en el directorio
if [[ -f "$directory/registry.db" ]]; then
echo "gitea_push_directory: añadiendo registry.db al .gitignore..." >&2
if [[ ! -f "$directory/.gitignore" ]] || ! grep -qxF "registry.db" "$directory/.gitignore"; then
echo "registry.db" >> "$directory/.gitignore"
fi
fi
# Gestionar estado del repositorio git
if [[ -d "$directory/.git" ]]; then
local existing_remote
existing_remote=$(git -C "$directory" remote get-url origin 2>/dev/null || echo "")
if [[ -z "$existing_remote" ]]; then
echo "gitea_push_directory: añadiendo remote origin..." >&2
git -C "$directory" remote add origin "$remote_url"
else
# Comparar remote sin token para detectar si apunta al mismo repo
local clean_existing
clean_existing=$(echo "$existing_remote" | sed 's|https://[^@]*@||;s|https://||')
local clean_target="${gitea_host}/${owner}/${repo}.git"
if [[ "$clean_existing" != "$clean_target" ]]; then
echo "gitea_push_directory: remote apunta a otro destino ('$clean_existing'), actualizando..." >&2
git -C "$directory" remote set-url origin "$remote_url"
else
echo "gitea_push_directory: remote ya apunta al destino correcto, actualizando token..." >&2
git -C "$directory" remote set-url origin "$remote_url"
fi
fi
else
echo "gitea_push_directory: inicializando nuevo repositorio git..." >&2
git -C "$directory" init
git -C "$directory" remote add origin "$remote_url"
fi
# Configurar rama por defecto
git -C "$directory" checkout -B "$branch" 2>/dev/null || true
# Añadir y commitear cambios si los hay
git -C "$directory" add -A
local status
status=$(git -C "$directory" status --porcelain)
if [[ -n "$status" ]]; then
echo "gitea_push_directory: commiteando cambios..." >&2
git -C "$directory" -c user.email="agent@fn-registry" -c user.name="fn-registry agent" \
commit -m "chore: sync from fn-registry agent"
else
echo "gitea_push_directory: sin cambios pendientes, solo haciendo push..." >&2
fi
echo "gitea_push_directory: haciendo push a $display_url..." >&2
git -C "$directory" push --set-upstream origin "$branch" --force-with-lease 2>&1 \
| sed "s|${GITEA_TOKEN}|***|g" >&2 \
|| git -C "$directory" push --set-upstream origin "$branch" 2>&1 \
| sed "s|${GITEA_TOKEN}|***|g" >&2
echo "gitea_push_directory: push completado a '$owner/$repo' rama '$branch'" >&2
}
@@ -0,0 +1,63 @@
---
name: install_android_sdk
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_android_sdk() -> void"
description: "Descarga e instala Android SDK command-line tools y JDK 17 localmente (sin root/sudo) en $ANDROID_SDK_DIR (default: $HOME/android-sdk). Idempotente: detecta instalacion existente y sale sin hacer nada. Genera env.sh con JAVA_HOME, ANDROID_HOME y PATH listos para hacer source."
tags: [android, sdk, jdk, java, install, infra, mobile]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params: []
output: "sin salida estructurada; imprime progreso y resumen final con rutas de instalacion"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_android_sdk.sh"
---
## Ejemplo
```bash
# Instalacion en directorio por defecto ($HOME/android-sdk)
source install_android_sdk.sh
# Instalacion en directorio personalizado
ANDROID_SDK_DIR=/opt/android source install_android_sdk.sh
# Si ya esta instalado:
# Android SDK ya instalado en: /home/user/android-sdk
# Instalacion completa imprime:
# Descargando JDK 17...
# JDK 17 instalado: /home/user/android-sdk/jdk-17/jdk-17.0.x+y
# Descargando Android cmdline-tools...
# cmdline-tools instalados
# Aceptando licencias de Android SDK...
# Instalando platform-tools, platforms;android-34, build-tools;34.0.0...
#
# Android SDK instalado en: /home/user/android-sdk
# JDK 17: /home/user/android-sdk/jdk-17/jdk-17.0.x+y
# Para activar: source /home/user/android-sdk/env.sh
# Activar entorno en sesion actual
source ~/android-sdk/env.sh
```
## Notas
Requiere `curl` y `unzip` (disponibles en la mayoria de distros Linux). No requiere root ni sudo.
El JDK se descarga desde Adoptium (Eclipse Temurin) via su API oficial. La URL de cmdline-tools apunta a la version 11076708 (2024). Si Google actualiza la version, cambiar la URL con el nuevo numero de build.
La reorganizacion del zip es necesaria porque Google distribuye cmdline-tools con estructura `cmdline-tools/bin/...` pero sdkmanager espera estar en `cmdline-tools/latest/bin/sdkmanager` para que Android Studio y otras herramientas lo detecten correctamente.
El archivo `env.sh` generado en `$ANDROID_SDK_DIR/env.sh` contiene las variables de entorno necesarias (`JAVA_HOME`, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, `PATH`) y puede hacerse source desde `.bashrc`, `.zshrc` o desde scripts de CI.
Paquetes instalados: `platform-tools` (adb, fastboot), `platforms;android-34` (API 34), `build-tools;34.0.0`.
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# install_android_sdk — Descarga e instala Android SDK command-line tools y JDK 17
# localmente (sin root/sudo) en $ANDROID_SDK_DIR (default: $HOME/android-sdk).
set -euo pipefail
install_android_sdk() {
local sdk_dir="${ANDROID_SDK_DIR:-$HOME/android-sdk}"
local tmp_dir
tmp_dir="$(mktemp -d)"
# Limpia temporales al salir
trap 'rm -rf "$tmp_dir"' EXIT
# 1. Verifica si ya está instalado
if [[ -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
if JAVA_HOME="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1)" \
"$sdk_dir/cmdline-tools/latest/bin/sdkmanager" --version &>/dev/null; then
echo "Android SDK ya instalado en: $sdk_dir"
return 0
fi
fi
mkdir -p "$sdk_dir"
# 2. Descarga JDK 17 si no existe
local jdk_dir
jdk_dir="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1 || true)"
if [[ -z "$jdk_dir" ]]; then
echo "Descargando JDK 17..."
local jdk_tar="$tmp_dir/jdk17.tar.gz"
local jdk_url="https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/jdk/hotspot/normal/eclipse"
if ! curl -fL --progress-bar -o "$jdk_tar" "$jdk_url"; then
echo "ERROR: fallo al descargar JDK 17 desde $jdk_url" >&2
return 1
fi
mkdir -p "$sdk_dir/jdk-17"
echo "Extrayendo JDK 17..."
if ! tar -xzf "$jdk_tar" -C "$sdk_dir/jdk-17"; then
echo "ERROR: fallo al extraer JDK 17" >&2
return 1
fi
jdk_dir="$(ls -d "$sdk_dir"/jdk-17/jdk-17* 2>/dev/null | head -1 || true)"
if [[ -z "$jdk_dir" ]]; then
echo "ERROR: no se encontro directorio jdk-17* tras la extraccion" >&2
return 1
fi
if ! JAVA_HOME="$jdk_dir" "$jdk_dir/bin/java" -version &>/dev/null; then
echo "ERROR: java -version fallo tras instalar JDK" >&2
return 1
fi
echo "JDK 17 instalado: $jdk_dir"
else
echo "JDK 17 ya presente: $jdk_dir"
fi
export JAVA_HOME="$jdk_dir"
# 3. Descarga Android cmdline-tools si no existen
if [[ ! -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
echo "Descargando Android cmdline-tools..."
local tools_zip="$tmp_dir/cmdline-tools.zip"
local tools_url="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
if ! curl -fL --progress-bar -o "$tools_zip" "$tools_url"; then
echo "ERROR: fallo al descargar Android cmdline-tools desde $tools_url" >&2
return 1
fi
local tools_tmp="$tmp_dir/cmdline-tools-extracted"
mkdir -p "$tools_tmp"
echo "Extrayendo cmdline-tools..."
if ! unzip -q "$tools_zip" -d "$tools_tmp"; then
echo "ERROR: fallo al extraer cmdline-tools" >&2
return 1
fi
# La estructura del zip es cmdline-tools/bin/..., reorganizar a cmdline-tools/latest/
mkdir -p "$sdk_dir/cmdline-tools"
if [[ -d "$tools_tmp/cmdline-tools" ]]; then
mv "$tools_tmp/cmdline-tools" "$sdk_dir/cmdline-tools/latest"
else
echo "ERROR: estructura inesperada en el zip de cmdline-tools" >&2
return 1
fi
if [[ ! -f "$sdk_dir/cmdline-tools/latest/bin/sdkmanager" ]]; then
echo "ERROR: sdkmanager no encontrado tras extraer cmdline-tools" >&2
return 1
fi
echo "cmdline-tools instalados"
else
echo "cmdline-tools ya presentes"
fi
local sdkmanager="$sdk_dir/cmdline-tools/latest/bin/sdkmanager"
export ANDROID_HOME="$sdk_dir"
export ANDROID_SDK_ROOT="$sdk_dir"
export PATH="$JAVA_HOME/bin:$sdk_dir/cmdline-tools/latest/bin:$sdk_dir/platform-tools:$PATH"
# 4. Acepta licencias e instala paquetes necesarios
echo "Aceptando licencias de Android SDK..."
if ! yes | "$sdkmanager" --licenses; then
echo "ERROR: fallo al aceptar licencias de Android SDK" >&2
return 1
fi
echo "Instalando platform-tools, platforms;android-34, build-tools;34.0.0..."
if ! "$sdkmanager" "platform-tools" "platforms;android-34" "build-tools;34.0.0"; then
echo "ERROR: fallo al instalar paquetes de Android SDK" >&2
return 1
fi
# 5. Genera archivo de entorno
local env_file="$sdk_dir/env.sh"
cat > "$env_file" <<EOF
export JAVA_HOME="$JAVA_HOME"
export ANDROID_HOME="$sdk_dir"
export ANDROID_SDK_ROOT="$sdk_dir"
export PATH="\$JAVA_HOME/bin:$sdk_dir/cmdline-tools/latest/bin:$sdk_dir/platform-tools:\$PATH"
EOF
# 6. Resumen final
echo ""
echo "Android SDK instalado en: $sdk_dir"
echo "JDK 17: $JAVA_HOME"
echo "Para activar: source $sdk_dir/env.sh"
}
install_android_sdk
+37
View File
@@ -0,0 +1,37 @@
---
name: install_cpp_deps
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_cpp_deps() -> void"
description: "Verifica e instala las dependencias de sistema necesarias para compilar C++ con ImGui (cmake, g++, glfw, mesa)"
tags: [cpp, dependencies, setup, cmake, imgui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_cpp_deps.sh"
params: []
output: "Instala paquetes faltantes via apt o confirma que todo esta instalado"
---
# install_cpp_deps
Verifica las dependencias necesarias para el build C++:
- `cmake` — sistema de build
- `g++` / `build-essential` — compilador
- `libglfw3-dev` — windowing (GLFW)
- `libgl1-mesa-dev` — OpenGL headers
Tambien reporta si `mingw-w64` esta disponible para cross-compile a Windows.
```bash
fn run install_cpp_deps
```
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[install_cpp_deps] Checking C++ build dependencies..."
MISSING=()
if ! command -v cmake &>/dev/null; then
MISSING+=(cmake)
else
echo " cmake: $(cmake --version | head -1)"
fi
if ! command -v g++ &>/dev/null; then
MISSING+=(g++ build-essential)
else
echo " g++: $(g++ --version | head -1)"
fi
if ! dpkg -s libglfw3-dev &>/dev/null 2>&1; then
MISSING+=(libglfw3-dev)
else
echo " libglfw3-dev: installed"
fi
if ! dpkg -s libgl1-mesa-dev &>/dev/null 2>&1; then
MISSING+=(libgl1-mesa-dev)
else
echo " libgl1-mesa-dev: installed"
fi
# Optional: mingw for cross-compile
if command -v x86_64-w64-mingw32-g++ &>/dev/null; then
echo " mingw-w64: $(x86_64-w64-mingw32-g++ --version | head -1)"
else
echo " mingw-w64: not installed (optional, for Windows cross-compile)"
fi
if [ ${#MISSING[@]} -eq 0 ]; then
echo "[install_cpp_deps] All dependencies satisfied."
exit 0
fi
echo ""
echo "[install_cpp_deps] Missing packages: ${MISSING[*]}"
echo "[install_cpp_deps] Installing..."
sudo apt-get update -qq
sudo apt-get install -y -qq "${MISSING[@]}"
echo "[install_cpp_deps] Done."
+40
View File
@@ -0,0 +1,40 @@
---
name: install_mantine
kind: function
lang: bash
domain: infra
version: "1.0.0"
purity: impure
signature: "install_mantine(project_dir: string) -> void"
description: "Instala Mantine UI con todas sus dependencias (@mantine/core, hooks, charts, notifications, form) y PostCSS en un proyecto frontend. Detecta package manager por lockfile. Genera postcss.config.cjs si no existe. Idempotente."
tags: [mantine, frontend, install, react, ui, postcss]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: project_dir
desc: "directorio del proyecto frontend con package.json"
output: "sin salida; muestra progreso de instalación"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/infra/install_mantine.sh"
---
## Ejemplo
```bash
# Instalar Mantine en un proyecto con pnpm
source install_mantine.sh
install_mantine ./apps/rapid_dashboards/frontend
# Uso directo
bash install_mantine.sh ./frontend
```
## Notas
Detecta el package manager por lockfile: pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm. Instala las dependencias core de Mantine v7+ y el stack PostCSS necesario. Si postcss.config.cjs ya existe no lo sobreescribe.
+97
View File
@@ -0,0 +1,97 @@
# install_mantine
# ---------------
# Instala dependencias de Mantine UI en un proyecto frontend.
# Detecta package manager por lockfile (pnpm > yarn > npm).
# Genera postcss.config.cjs si no existe.
# Idempotente: no reinstala si ya estan presentes.
#
# USO (sourced):
# source install_mantine.sh
# install_mantine /path/to/frontend
#
# USO (directo):
# bash install_mantine.sh /path/to/frontend
install_mantine() {
local project_dir="$1"
if [ -z "$project_dir" ]; then
echo "install_mantine: se requiere project_dir" >&2
return 1
fi
if [ ! -f "$project_dir/package.json" ]; then
echo "install_mantine: no existe package.json en $project_dir" >&2
return 1
fi
# Detectar package manager
local pm="npm"
local add_cmd="install"
local add_dev_flag="--save-dev"
if [ -f "$project_dir/pnpm-lock.yaml" ] || [ -f "$project_dir/pnpm-workspace.yaml" ]; then
pm="pnpm"
add_cmd="add"
add_dev_flag="-D"
elif [ -f "$project_dir/yarn.lock" ]; then
pm="yarn"
add_cmd="add"
add_dev_flag="--dev"
elif [ -f "$project_dir/package-lock.json" ]; then
pm="npm"
add_cmd="install"
add_dev_flag="--save-dev"
fi
echo "Detectado package manager: $pm"
# Dependencias runtime
local runtime_deps="@mantine/core @mantine/hooks @mantine/charts @mantine/notifications @mantine/form"
echo "Instalando dependencias Mantine..."
(cd "$project_dir" && $pm $add_cmd $runtime_deps 2>&1)
if [ $? -ne 0 ]; then
echo "install_mantine: fallo instalando dependencias runtime" >&2
return 1
fi
# Dependencias PostCSS (dev)
local dev_deps="postcss postcss-preset-mantine postcss-simple-vars"
echo "Instalando dependencias PostCSS..."
(cd "$project_dir" && $pm $add_cmd $add_dev_flag $dev_deps 2>&1)
if [ $? -ne 0 ]; then
echo "install_mantine: fallo instalando dependencias PostCSS" >&2
return 1
fi
# Generar postcss.config.cjs si no existe
if [ ! -f "$project_dir/postcss.config.cjs" ]; then
echo "Generando postcss.config.cjs..."
cat > "$project_dir/postcss.config.cjs" << 'POSTCSS'
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
};
POSTCSS
echo "postcss.config.cjs creado"
else
echo "postcss.config.cjs ya existe, no se sobreescribe"
fi
echo "Mantine instalado correctamente en $project_dir"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
install_mantine "$@"
fi
@@ -0,0 +1,76 @@
---
name: capacitor_build_apk
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "capacitor_build_apk(web_app_dir: string, [app_id: string], [app_name: string]) -> void"
description: "Pipeline que convierte una web app en un APK de Android usando Capacitor. Valida el entorno (ANDROID_HOME, Java 17+), construye el bundle web si no existe dist/, inicializa Capacitor si no está configurado, añade la plataforma Android, sincroniza y compila el APK con Gradle. El APK final queda en el directorio raíz de la web app."
tags: [android, apk, capacitor, mobile, build, pipeline, bash]
uses_functions:
- install_android_sdk_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: web_app_dir
desc: "directorio raíz de la web app; debe contener package.json; si no existe dist/ se ejecuta pnpm build automáticamente"
- name: app_id
desc: "identificador de la app Android en formato reverse-DNS (default: com.fnregistry.app)"
- name: app_name
desc: "nombre visible de la app Android; si se omite, se lee del campo name de package.json"
output: "APK de debug en <web_app_dir>/<app_name>.apk; imprime ruta y tamaño en MB al finalizar"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/capacitor_build_apk.sh"
---
## Ejemplo
```bash
# Build con defaults (app-id y app-name desde package.json)
./bash/functions/pipelines/capacitor_build_apk.sh ~/projects/my-web-app
# Build especificando app-id y app-name
./bash/functions/pipelines/capacitor_build_apk.sh ~/projects/my-web-app \
--app-id com.miempresa.miapp \
--app-name "Mi Aplicación"
```
## Flujo
1. **Validación** — verifica que `web_app_dir` existe, tiene `package.json`, que `ANDROID_HOME` está seteado (o sourcea `$HOME/android-sdk/env.sh`) y que Java 17+ está disponible.
2. **Build web** — si no existe `dist/`, ejecuta `pnpm build` en el directorio de la app.
3. **Init Capacitor** — si no existe `capacitor.config.ts`, instala `@capacitor/core`, `@capacitor/cli` y `@capacitor/android` via npm y genera el archivo de configuración con el `appId`, `appName` y `webDir: dist`.
4. **Add Android** — si no existe el directorio `android/`, ejecuta `npx cap add android`.
5. **Sync** — ejecuta `npx cap sync android` para copiar los assets web al proyecto Android.
6. **Build APK** — ejecuta `./gradlew assembleDebug` desde `android/`; si falla sale con exit 1.
7. **Copia APK** — copia `android/app/build/outputs/apk/debug/app-debug.apk` a `<web_app_dir>/<app_name>.apk`.
8. **Resultado** — imprime la ruta del APK y su tamaño en MB.
## Requisitos
- **Node.js** y **pnpm** disponibles en PATH
- **Java 17+** disponible en PATH
- **Android SDK** instalado: `ANDROID_HOME` seteado, o bien `$HOME/android-sdk/env.sh` existente (generado por `install_android_sdk`)
- **Gradle wrapper** presente en el directorio `android/` (generado por `cap add android`)
## Notas
El pipeline usa `set -euo pipefail` — cualquier fallo detiene la ejecución inmediatamente.
El APK generado es un **debug build**, apto para desarrollo y pruebas. Para publicar en Play Store se necesita un release build firmado (`assembleRelease` con un keystore).
`install_android_sdk_bash_infra` se referencia como dependencia previa: el usuario debe haberlo ejecutado (o haber instalado el SDK manualmente) antes de invocar este pipeline.
La detección del `app_name` desde `package.json` usa `node -e` inline, lo que requiere que Node.js esté disponible. Si el campo `name` no existe en el JSON, se usa el valor por defecto `app`.
Para instalar el APK en un dispositivo Android conectado por USB (con depuración USB activada):
```bash
adb install <web_app_dir>/<app_name>.apk
```
@@ -0,0 +1,208 @@
#!/usr/bin/env bash
# capacitor_build_apk
# -------------------
# Pipeline que convierte una web app buildeada en un APK de Android usando Capacitor.
# Asume que el Android SDK está instalado (via install_android_sdk o manualmente).
#
# USO:
# ./capacitor_build_apk.sh <web_app_dir> [--app-id com.example.app] [--app-name "My App"]
#
# ARGUMENTOS:
# web_app_dir Directorio de la web app (debe contener package.json)
# --app-id ID de la app Android (default: com.fnregistry.app)
# --app-name Nombre visible de la app (default: name de package.json)
#
# REQUISITOS:
# - Node.js + pnpm instalados en PATH
# - Java 17+ instalado en PATH
# - Android SDK: ANDROID_HOME seteado o $HOME/android-sdk/env.sh disponible
set -euo pipefail
# ---------------------------------------------------------------------------
# Parseo de argumentos
# ---------------------------------------------------------------------------
WEB_APP_DIR=""
APP_ID="com.fnregistry.app"
APP_NAME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--app-id)
APP_ID="$2"
shift 2
;;
--app-name)
APP_NAME="$2"
shift 2
;;
-*)
echo "[capacitor_build_apk] ERROR: argumento desconocido: $1" >&2
echo "USO: $0 <web_app_dir> [--app-id com.example.app] [--app-name \"My App\"]" >&2
exit 1
;;
*)
WEB_APP_DIR="$1"
shift
;;
esac
done
if [[ -z "$WEB_APP_DIR" ]]; then
echo "[capacitor_build_apk] ERROR: web_app_dir es obligatorio." >&2
echo "USO: $0 <web_app_dir> [--app-id com.example.app] [--app-name \"My App\"]" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# 1. Validación
# ---------------------------------------------------------------------------
echo "[capacitor_build_apk] Validando entorno..."
# Verificar que web_app_dir existe y tiene package.json
if [[ ! -d "$WEB_APP_DIR" ]]; then
echo "[capacitor_build_apk] ERROR: directorio no existe: $WEB_APP_DIR" >&2
exit 1
fi
if [[ ! -f "$WEB_APP_DIR/package.json" ]]; then
echo "[capacitor_build_apk] ERROR: no se encontró package.json en $WEB_APP_DIR" >&2
exit 1
fi
# Resolver app name desde package.json si no se pasó
if [[ -z "$APP_NAME" ]]; then
APP_NAME=$(node -e "const p = require('$WEB_APP_DIR/package.json'); process.stdout.write(p.name || 'app');" 2>/dev/null || echo "app")
echo "[capacitor_build_apk] App name detectado desde package.json: $APP_NAME"
fi
# Verificar ANDROID_HOME o sourcea env.sh
if [[ -z "${ANDROID_HOME:-}" ]]; then
ANDROID_ENV="$HOME/android-sdk/env.sh"
if [[ -f "$ANDROID_ENV" ]]; then
echo "[capacitor_build_apk] ANDROID_HOME no seteado, sourceando $ANDROID_ENV ..."
# shellcheck source=/dev/null
source "$ANDROID_ENV"
else
echo "[capacitor_build_apk] ERROR: ANDROID_HOME no está seteado y no se encontró $ANDROID_ENV" >&2
echo " Instala el SDK con install_android_sdk o setea ANDROID_HOME manualmente." >&2
exit 1
fi
fi
echo "[capacitor_build_apk] ANDROID_HOME: $ANDROID_HOME"
# Verificar Java 17+
if ! command -v java &>/dev/null; then
echo "[capacitor_build_apk] ERROR: java no está en PATH." >&2
exit 1
fi
JAVA_VERSION=$(java -version 2>&1 | head -1 | grep -oP '(?<=version ")([0-9]+)' | head -1 || echo "0")
if [[ "$JAVA_VERSION" -lt 17 ]]; then
echo "[capacitor_build_apk] ERROR: se requiere Java 17+. Versión detectada: $JAVA_VERSION" >&2
exit 1
fi
echo "[capacitor_build_apk] Java $JAVA_VERSION detectado."
# ---------------------------------------------------------------------------
# 2. Build web (si no existe dist/)
# ---------------------------------------------------------------------------
if [[ ! -d "$WEB_APP_DIR/dist" ]]; then
echo "[capacitor_build_apk] No se encontró dist/, ejecutando pnpm build..."
(cd "$WEB_APP_DIR" && pnpm build)
echo "[capacitor_build_apk] Build web completado."
else
echo "[capacitor_build_apk] dist/ ya existe, omitiendo build web."
fi
# ---------------------------------------------------------------------------
# 3. Init Capacitor (si no existe capacitor.config.ts)
# ---------------------------------------------------------------------------
if [[ ! -f "$WEB_APP_DIR/capacitor.config.ts" ]]; then
echo "[capacitor_build_apk] Instalando dependencias de Capacitor..."
(cd "$WEB_APP_DIR" && npm install @capacitor/core @capacitor/cli @capacitor/android)
echo "[capacitor_build_apk] Generando capacitor.config.ts..."
cat > "$WEB_APP_DIR/capacitor.config.ts" <<CAPCONFIG
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: '${APP_ID}',
appName: '${APP_NAME}',
webDir: 'dist',
server: { androidScheme: 'https' }
};
export default config;
CAPCONFIG
echo "[capacitor_build_apk] capacitor.config.ts generado."
else
echo "[capacitor_build_apk] capacitor.config.ts ya existe, omitiendo init."
fi
# ---------------------------------------------------------------------------
# 4. Add Android (si no existe el directorio android/)
# ---------------------------------------------------------------------------
if [[ ! -d "$WEB_APP_DIR/android" ]]; then
echo "[capacitor_build_apk] Añadiendo plataforma Android..."
(cd "$WEB_APP_DIR" && npx cap add android)
echo "[capacitor_build_apk] Plataforma Android añadida."
else
echo "[capacitor_build_apk] Directorio android/ ya existe, omitiendo cap add."
fi
# ---------------------------------------------------------------------------
# 5. Sync
# ---------------------------------------------------------------------------
echo "[capacitor_build_apk] Sincronizando assets web con Android..."
(cd "$WEB_APP_DIR" && npx cap sync android)
echo "[capacitor_build_apk] Sync completado."
# ---------------------------------------------------------------------------
# 6. Build APK
# ---------------------------------------------------------------------------
echo "[capacitor_build_apk] Compilando APK con Gradle..."
if ! (cd "$WEB_APP_DIR/android" && ./gradlew assembleDebug); then
echo "[capacitor_build_apk] ERROR: Gradle falló. Revisa los logs anteriores." >&2
exit 1
fi
APK_SOURCE="$WEB_APP_DIR/android/app/build/outputs/apk/debug/app-debug.apk"
if [[ ! -f "$APK_SOURCE" ]]; then
echo "[capacitor_build_apk] ERROR: Gradle terminó sin error pero no se encontró el APK en $APK_SOURCE" >&2
exit 1
fi
# ---------------------------------------------------------------------------
# 7. Copia APK al directorio raíz
# ---------------------------------------------------------------------------
APK_DEST="$WEB_APP_DIR/${APP_NAME}.apk"
cp "$APK_SOURCE" "$APK_DEST"
# ---------------------------------------------------------------------------
# 8. Resultado
# ---------------------------------------------------------------------------
APK_SIZE_BYTES=$(stat -c%s "$APK_DEST" 2>/dev/null || stat -f%z "$APK_DEST" 2>/dev/null || echo "0")
APK_SIZE_MB=$(awk "BEGIN {printf \"%.1f\", $APK_SIZE_BYTES/1048576}")
echo ""
echo "---------------------------------------------------------------------"
echo "APK generado: $APK_DEST"
echo "Tamaño: ${APK_SIZE_MB} MB"
echo ""
echo "Para instalar en un dispositivo conectado por USB:"
echo " adb install '$APK_DEST'"
echo "---------------------------------------------------------------------"
@@ -0,0 +1,67 @@
---
name: gitea_init_app
kind: pipeline
lang: bash
domain: pipelines
version: "1.0.0"
purity: impure
signature: "gitea_init_app(directory: string, owner: string, name: string, private: string) -> string"
description: "Pipeline que crea un repositorio en Gitea, sube el directorio local y añade a egutierrez como colaborador admin. Compone gitea_create_repo → gitea_push_directory → gitea_add_collaborator."
tags: [gitea, git, pipeline, repo, create, push, launcher, infra]
uses_functions:
- gitea_create_repo_bash_infra
- gitea_push_directory_bash_infra
- gitea_add_collaborator_bash_infra
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: directory
desc: "ruta al directorio local a subir como repositorio"
- name: owner
desc: "usuario u organización en Gitea que será propietaria del repo"
- name: name
desc: "nombre del repositorio (opcional: se infiere del basename del directorio)"
- name: private
desc: "si el repo debe ser privado, 'true' o 'false' (default: false)"
output: "URL del repositorio creado en Gitea"
tested: false
tests: []
test_file_path: ""
file_path: "bash/functions/pipelines/gitea_init_app.sh"
---
## Ejemplo
```bash
export GITEA_URL="$(pass agentes/gitea-url)"
export GITEA_TOKEN="$(pass agentes/dataforge-token)"
# Crear repo con nombre inferido del directorio
bash bash/functions/pipelines/gitea_init_app.sh /home/lucas/myapp myorg
# Nombre explícito y repo privado
bash bash/functions/pipelines/gitea_init_app.sh /home/lucas/myapp myorg my-custom-name true
# Con flags
bash bash/functions/pipelines/gitea_init_app.sh \
--directory /home/lucas/myapp \
--owner myorg \
--name my-app \
--private true
```
## Pasos del pipeline
1. `gitea_create_repo owner name private` — crea el repo (idempotente si ya existe)
2. `gitea_push_directory directory owner repo` — inicializa git y hace push del directorio
3. `gitea_add_collaborator owner repo egutierrez admin` — añade colaborador con permisos admin
## Notas
- Requiere `GITEA_URL` y `GITEA_TOKEN` seteadas.
- Si el repo ya existe (409), el pipeline continúa con el push y añade el colaborador.
- El colaborador `egutierrez` es fijo en el pipeline — para variarlo usar las funciones individuales.
- La URL del repo se imprime a stdout al finalizar.
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Pipeline: gitea_init_app — Crea repo en Gitea, sube directorio y añade colaborador egutierrez
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../infra/gitea_create_repo.sh"
source "$SCRIPT_DIR/../infra/gitea_push_directory.sh"
source "$SCRIPT_DIR/../infra/gitea_add_collaborator.sh"
main() {
local directory=""
local owner=""
local name=""
local private="false"
# Parsear argumentos
while [[ $# -gt 0 ]]; do
case "$1" in
--directory) directory="$2"; shift 2 ;;
--owner) owner="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--private) private="$2"; shift 2 ;;
*)
# Argumentos posicionales: directory owner [name] [private]
if [[ -z "$directory" ]]; then
directory="$1"
elif [[ -z "$owner" ]]; then
owner="$1"
elif [[ -z "$name" ]]; then
name="$1"
else
private="$1"
fi
shift
;;
esac
done
if [[ -z "$directory" || -z "$owner" ]]; then
echo "gitea_init_app: uso: gitea_init_app <directory> <owner> [name] [private]" >&2
echo "gitea_init_app: o con flags: --directory <dir> --owner <owner> [--name <name>] [--private true]" >&2
return 1
fi
# Inferir nombre del repo desde basename del directorio si no se especificó
if [[ -z "$name" ]]; then
name=$(basename "$directory")
echo "gitea_init_app: nombre inferido del directorio: '$name'" >&2
fi
if [[ -z "${GITEA_URL:-}" ]]; then
echo "gitea_init_app: GITEA_URL no está seteada" >&2
return 1
fi
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "gitea_init_app: GITEA_TOKEN no está seteado" >&2
return 1
fi
echo "gitea_init_app: iniciando pipeline para '$owner/$name'..." >&2
echo "gitea_init_app: directorio fuente: '$directory'" >&2
# Paso 1: Crear repo
echo "gitea_init_app: [1/3] creando repositorio..." >&2
gitea_create_repo "$owner" "$name" "$private" "" > /dev/null
# Paso 2: Subir directorio
echo "gitea_init_app: [2/3] subiendo directorio al repositorio..." >&2
gitea_push_directory "$directory" "$owner" "$name"
# Paso 3: Añadir colaborador egutierrez con permisos admin
echo "gitea_init_app: [3/3] añadiendo colaborador egutierrez..." >&2
gitea_add_collaborator "$owner" "$name" "egutierrez" "admin"
echo "gitea_init_app: pipeline completado — ${GITEA_URL}/${owner}/${name}" >&2
echo "${GITEA_URL}/${owner}/${name}"
}
main "$@"
+128
View File
@@ -0,0 +1,128 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"fn-registry/registry"
)
func buildCppCommand(fn *registry.Function, registryRoot, absPath string, args []string) (*exec.Cmd, error) {
cppRoot := filepath.Join(registryRoot, "cpp")
buildDir := filepath.Join(cppRoot, "build", "linux")
// Ensure build directory exists and cmake is configured
if _, err := os.Stat(filepath.Join(buildDir, "CMakeCache.txt")); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "[fn run] configuring cmake for cpp...\n")
configure := exec.Command("cmake", "-B", buildDir, "-S", cppRoot)
configure.Dir = registryRoot
configure.Stdout = os.Stderr
configure.Stderr = os.Stderr
if err := configure.Run(); err != nil {
return nil, fmt.Errorf("cmake configure failed: %w", err)
}
}
dir := filepath.Dir(absPath)
// Check if the function's directory has its own CMakeLists.txt (app with main)
localCMake := filepath.Join(dir, "CMakeLists.txt")
hasMain := false
if _, err := os.Stat(localCMake); err == nil {
hasMain = true
}
// Also check for main.cpp in the same directory
mainCpp := filepath.Join(dir, "main.cpp")
if _, err := os.Stat(mainCpp); err == nil {
hasMain = true
}
if hasMain {
// Build and run the app binary
targetName := filepath.Base(dir)
build := exec.Command("cmake", "--build", buildDir, "--target", targetName)
build.Dir = registryRoot
build.Stdout = os.Stderr
build.Stderr = os.Stderr
fmt.Fprintf(os.Stderr, "[fn run] building target %s...\n", targetName)
if err := build.Run(); err != nil {
return nil, fmt.Errorf("cmake build failed: %w", err)
}
// Find the built binary
binaryPath := findBinary(buildDir, targetName)
if binaryPath == "" {
return nil, fmt.Errorf("built binary %q not found in %s", targetName, buildDir)
}
cmd := exec.Command(binaryPath, args...)
cmd.Dir = dir
return cmd, nil
}
// Library code: compile-check only (like go vet)
build := exec.Command("cmake", "--build", buildDir)
build.Dir = registryRoot
build.Stdout = os.Stderr
build.Stderr = os.Stderr
fmt.Fprintf(os.Stderr, "[fn run] %s is library code — running compile check\n", fn.ID)
if err := build.Run(); err != nil {
return nil, fmt.Errorf("compile check failed: %w", err)
}
// Return a no-op command that just prints success
cmd := exec.Command("echo", fmt.Sprintf("[fn run] %s compiled successfully", fn.ID))
return cmd, nil
}
// findBinary searches for an executable in the build tree.
func findBinary(buildDir, name string) string {
// Common locations cmake puts binaries
candidates := []string{
filepath.Join(buildDir, name),
filepath.Join(buildDir, "apps", name, name),
}
for _, c := range candidates {
if info, err := os.Stat(c); err == nil && !info.IsDir() {
// Check if executable
if info.Mode()&0111 != 0 {
return c
}
}
}
// Walk the build directory as fallback
var found string
filepath.Walk(buildDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
if info.Name() == name && info.Mode()&0111 != 0 {
found = path
return filepath.SkipAll
}
return nil
})
// Also try without extension match for paths with subdirectories
if found == "" {
filepath.Walk(buildDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
base := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
if base == name && info.Mode()&0111 != 0 {
found = path
return filepath.SkipAll
}
return nil
})
}
return found
}
+2
View File
@@ -103,6 +103,8 @@ func buildCommand(fn *registry.Function, db *registry.DB, registryRoot, absPath
return buildBashCommand(absPath, args)
case "ts":
return buildTsCommand(registryRoot, absPath, args)
case "cpp":
return buildCppCommand(fn, registryRoot, absPath, args)
default:
return nil, fmt.Errorf("unsupported lang %q for execution", fn.Lang)
}
+100
View File
@@ -0,0 +1,100 @@
cmake_minimum_required(VERSION 3.16)
project(fn_registry_cpp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- Options ---
option(TRACY_ENABLE "Enable Tracy profiling" OFF)
# --- Vendor: Dear ImGui ---
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/imgui)
add_library(imgui STATIC
${IMGUI_DIR}/imgui.cpp
${IMGUI_DIR}/imgui_draw.cpp
${IMGUI_DIR}/imgui_tables.cpp
${IMGUI_DIR}/imgui_widgets.cpp
${IMGUI_DIR}/imgui_demo.cpp
${IMGUI_DIR}/backends/imgui_impl_glfw.cpp
${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp
)
target_include_directories(imgui PUBLIC
${IMGUI_DIR}
${IMGUI_DIR}/backends
)
# --- Vendor: ImPlot ---
set(IMPLOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/implot)
add_library(implot STATIC
${IMPLOT_DIR}/implot.cpp
${IMPLOT_DIR}/implot_items.cpp
)
target_include_directories(implot PUBLIC ${IMPLOT_DIR})
target_link_libraries(implot PUBLIC imgui)
# --- Vendor: Tracy (optional) ---
if(TRACY_ENABLE)
set(TRACY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/tracy)
add_library(tracy STATIC
${TRACY_DIR}/public/TracyClient.cpp
)
target_include_directories(tracy PUBLIC ${TRACY_DIR}/public)
target_compile_definitions(tracy PUBLIC TRACY_ENABLE)
endif()
# --- Platform dependencies ---
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
# Cross-compile: use vendored or system GLFW, link opengl32/gdi32
find_package(glfw3 QUIET)
if(NOT glfw3_FOUND)
# Build GLFW from source if available
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/glfw/CMakeLists.txt)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
add_subdirectory(vendor/glfw)
else()
message(FATAL_ERROR "GLFW not found. For Windows cross-compile, add GLFW source to cpp/vendor/glfw/")
endif()
endif()
set(PLATFORM_LIBS glfw opengl32 gdi32 imm32)
else()
# Linux native
find_package(glfw3 REQUIRED)
find_package(OpenGL REQUIRED)
set(PLATFORM_LIBS glfw OpenGL::GL ${CMAKE_DL_LIBS})
endif()
target_link_libraries(imgui PUBLIC ${PLATFORM_LIBS})
# --- Framework ---
add_library(fn_framework STATIC
framework/app_base.cpp
)
target_include_directories(fn_framework PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/framework
${CMAKE_CURRENT_SOURCE_DIR}/functions
)
target_link_libraries(fn_framework PUBLIC imgui implot)
if(TRACY_ENABLE)
target_link_libraries(fn_framework PUBLIC tracy)
endif()
# --- Macro for creating ImGui apps ---
function(add_imgui_app target)
add_executable(${target} ${ARGN})
target_link_libraries(${target} PRIVATE fn_framework)
target_include_directories(${target} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/functions
)
endfunction()
# --- Function libraries (headers for composition) ---
# Functions are compiled as part of apps that use them via add_imgui_app.
# Each function is a .h/.cpp pair included by the app's CMakeLists.txt.
# --- Demo app ---
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/chart_demo/CMakeLists.txt)
add_subdirectory(apps/chart_demo)
endif()
+8
View File
@@ -0,0 +1,8 @@
add_imgui_app(chart_demo
main.cpp
${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
${CMAKE_SOURCE_DIR}/functions/viz/heatmap.cpp
${CMAKE_SOURCE_DIR}/functions/core/fps_overlay.cpp
)
+79
View File
@@ -0,0 +1,79 @@
#include "app_base.h"
#include "imgui.h"
#include "implot.h"
#include "viz/line_plot.h"
#include "viz/scatter_plot.h"
#include "viz/bar_chart.h"
#include "viz/heatmap.h"
#include "core/fps_overlay.h"
#include <cmath>
#include <vector>
// Generate sample data
static constexpr int N = 500;
static float xs[N], ys_sin[N], ys_cos[N];
static float scatter_x[200], scatter_y[200];
static const char* bar_labels[] = {"Go", "Python", "Bash", "TypeScript", "C++"};
static float bar_values[] = {201.0f, 202.0f, 38.0f, 80.0f, 5.0f};
static float heat_data[10 * 10];
static bool data_initialized = false;
static void init_data() {
if (data_initialized) return;
for (int i = 0; i < N; i++) {
xs[i] = static_cast<float>(i) * 0.02f;
ys_sin[i] = sinf(xs[i]);
ys_cos[i] = cosf(xs[i]);
}
for (int i = 0; i < 200; i++) {
scatter_x[i] = static_cast<float>(rand()) / RAND_MAX * 10.0f;
scatter_y[i] = scatter_x[i] * 0.5f + (static_cast<float>(rand()) / RAND_MAX - 0.5f) * 3.0f;
}
for (int i = 0; i < 100; i++) {
int r = i / 10, c = i % 10;
heat_data[i] = sinf(r * 0.5f) * cosf(c * 0.5f);
}
data_initialized = true;
}
static void render() {
init_data();
fps_overlay();
// Full-window dockspace
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
if (ImGui::Begin("fn_registry — Chart Demo")) {
if (ImGui::BeginTabBar("##charts")) {
if (ImGui::BeginTabItem("Line Plot")) {
ImGui::Text("sin(x) — %d points", N);
line_plot("Sine Wave", xs, ys_sin, N);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Scatter Plot")) {
ImGui::Text("y = 0.5x + noise — 200 points");
scatter_plot("Scatter Data", scatter_x, scatter_y, 200);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Bar Chart")) {
ImGui::Text("Functions per language in fn_registry");
bar_chart("Registry Languages", bar_labels, bar_values, 5);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Heatmap")) {
ImGui::Text("sin(r) * cos(c) — 10x10 matrix");
heatmap("Correlation Matrix", heat_data, 10, 10, -1.0f, 1.0f);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
}
int main() {
return fn::run_app({.title = "fn_registry — Chart Demo", .width = 1400, .height = 900}, render);
}
+105
View File
@@ -0,0 +1,105 @@
#include "app_base.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "implot.h"
#include <GLFW/glfw3.h>
#include <cstdio>
#ifdef TRACY_ENABLE
#include "tracy/Tracy.hpp"
#endif
static void glfw_error_callback(int error, const char* description) {
fprintf(stderr, "GLFW Error %d: %s\n", error, description);
}
namespace fn {
int run_app(AppConfig config, std::function<void()> render_fn) {
glfwSetErrorCallback(glfw_error_callback);
if (!glfwInit()) {
fprintf(stderr, "Failed to initialize GLFW\n");
return 1;
}
// OpenGL 3.3 core
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
GLFWwindow* window = glfwCreateWindow(config.width, config.height, config.title, nullptr, nullptr);
if (!window) {
fprintf(stderr, "Failed to create GLFW window\n");
glfwTerminate();
return 1;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(config.vsync ? 1 : 0);
// Setup ImGui
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 330");
// Main loop
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
if (glfwGetWindowAttrib(window, GLFW_ICONIFIED)) {
glfwWaitEvents();
continue;
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
render_fn();
ImGui::Render();
int display_w, display_h;
glfwGetFramebufferSize(window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
glClearColor(config.bg_r, config.bg_g, config.bg_b, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
#ifdef TRACY_ENABLE
FrameMark;
#endif
}
// Cleanup
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}
int run_app(std::function<void()> render_fn) {
return run_app(AppConfig{}, render_fn);
}
} // namespace fn
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <functional>
namespace fn {
struct AppConfig {
const char* title = "fn_registry";
int width = 1280;
int height = 720;
bool vsync = true;
float bg_r = 0.1f;
float bg_g = 0.1f;
float bg_b = 0.1f;
};
// Run an ImGui application. The render_fn is called every frame
// between ImGui::NewFrame() and ImGui::Render().
// Returns 0 on clean exit, 1 on error.
int run_app(AppConfig config, std::function<void()> render_fn);
// Convenience: run with default config
int run_app(std::function<void()> render_fn);
} // namespace fn
+33
View File
@@ -0,0 +1,33 @@
#include "core/fps_overlay.h"
#include "imgui.h"
#ifdef TRACY_ENABLE
#include "tracy/Tracy.hpp"
#endif
void fps_overlay() {
#ifdef TRACY_ENABLE
ZoneScoped;
#endif
ImGuiIO& io = ImGui::GetIO();
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_AlwaysAutoResize
| ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_NoFocusOnAppearing
| ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoMove;
const float pad = 10.0f;
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImVec2 pos(viewport->WorkPos.x + viewport->WorkSize.x - pad,
viewport->WorkPos.y + pad);
ImGui::SetNextWindowPos(pos, ImGuiCond_Always, ImVec2(1.0f, 0.0f));
ImGui::SetNextWindowBgAlpha(0.65f);
if (ImGui::Begin("##fps_overlay", nullptr, flags)) {
ImGui::Text("%.1f FPS", io.Framerate);
ImGui::Text("%.3f ms", 1000.0f / io.Framerate);
}
ImGui::End();
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
// Renders an FPS counter overlay in the top-right corner.
// Call within an ImGui frame.
void fps_overlay();
+30
View File
@@ -0,0 +1,30 @@
---
name: fps_overlay
kind: component
lang: cpp
domain: core
version: "1.0.0"
purity: pure
signature: "void fps_overlay()"
description: "Renderiza un overlay de FPS y frametime en la esquina superior derecha, con soporte opcional de Tracy"
tags: [imgui, fps, overlay, profiling, debug]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/fps_overlay.cpp"
framework: imgui
params: []
output: "Renderiza el overlay de FPS en el frame ImGui actual"
---
# fps_overlay
Muestra FPS y frametime (ms) en una ventana semi-transparente en la esquina superior derecha.
Si se compila con `TRACY_ENABLE`, incluye un `ZoneScoped` para profiling con Tracy.
+26
View File
@@ -0,0 +1,26 @@
#include "viz/bar_chart.h"
#include "implot.h"
#include <vector>
void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
std::vector<double> positions(count);
for (int i = 0; i < count; i++) positions[i] = i;
ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, labels);
ImPlot::PlotBars("##data", values, count, bar_width);
ImPlot::EndPlot();
}
}
void bar_chart(const char* title, const char* const* labels, const double* values, int count, double bar_width) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
std::vector<double> positions(count);
for (int i = 0; i < count; i++) positions[i] = i;
ImPlot::SetupAxisTicks(ImAxis_X1, positions.data(), count, labels);
ImPlot::PlotBars("##data", values, count, bar_width);
ImPlot::EndPlot();
}
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
// Renders a vertical bar chart using ImPlot.
// Call within an ImGui frame.
void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width = 0.67f);
void bar_chart(const char* title, const char* const* labels, const double* values, int count, double bar_width = 0.67);
+40
View File
@@ -0,0 +1,40 @@
---
name: bar_chart
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void bar_chart(const char* title, const char* const* labels, const float* values, int count, float bar_width)"
description: "Renderiza un grafico de barras verticales usando ImPlot dentro de un frame ImGui"
tags: [implot, chart, visualization, gpu, bar]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/bar_chart.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del grafico de barras"
- name: labels
desc: "Array de etiquetas para el eje X, una por barra"
- name: values
desc: "Array de valores numericos para la altura de cada barra"
- name: count
desc: "Numero de barras (longitud de labels y values)"
- name: bar_width
desc: "Ancho de cada barra como fraccion del espacio disponible (default 0.67)"
output: "Renderiza el grafico de barras en el frame ImGui actual"
---
# bar_chart
Wrapper atomico sobre `ImPlot::PlotBars` con configuracion automatica de etiquetas en el eje X.
Debe llamarse dentro del render callback de `fn::run_app`.
+24
View File
@@ -0,0 +1,24 @@
#include "viz/heatmap.h"
#include "implot.h"
void heatmap(const char* title, const float* values, int rows, int cols,
float scale_min, float scale_max) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_NoLegend)) {
ImPlot::SetupAxes(nullptr, nullptr,
ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
ImPlot::PlotHeatmap("##data", values, rows, cols,
scale_min, scale_max);
ImPlot::EndPlot();
}
}
void heatmap(const char* title, const double* values, int rows, int cols,
double scale_min, double scale_max) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0), ImPlotFlags_NoLegend)) {
ImPlot::SetupAxes(nullptr, nullptr,
ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
ImPlot::PlotHeatmap("##data", values, rows, cols,
scale_min, scale_max);
ImPlot::EndPlot();
}
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
// Renders a heatmap using ImPlot.
// Data is row-major: values[row * cols + col].
// Call within an ImGui frame.
void heatmap(const char* title, const float* values, int rows, int cols,
float scale_min = 0.0f, float scale_max = 0.0f);
void heatmap(const char* title, const double* values, int rows, int cols,
double scale_min = 0.0, double scale_max = 0.0);
+42
View File
@@ -0,0 +1,42 @@
---
name: heatmap
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void heatmap(const char* title, const float* values, int rows, int cols, float scale_min, float scale_max)"
description: "Renderiza un mapa de calor 2D usando ImPlot dentro de un frame ImGui"
tags: [implot, chart, visualization, gpu, heatmap, matrix]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/heatmap.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del heatmap"
- name: values
desc: "Array de valores en orden row-major (values[row * cols + col])"
- name: rows
desc: "Numero de filas de la matriz"
- name: cols
desc: "Numero de columnas de la matriz"
- name: scale_min
desc: "Valor minimo de la escala de color (0 para autodetectar)"
- name: scale_max
desc: "Valor maximo de la escala de color (0 para autodetectar)"
output: "Renderiza el heatmap en el frame ImGui actual"
---
# heatmap
Wrapper atomico sobre `ImPlot::PlotHeatmap`. Renderiza una matriz de valores como mapa de calor con escala de color.
Los datos deben estar en formato row-major. Si `scale_min` y `scale_max` son ambos 0, ImPlot autodetecta el rango.
+16
View File
@@ -0,0 +1,16 @@
#include "viz/line_plot.h"
#include "implot.h"
void line_plot(const char* title, const float* xs, const float* ys, int count) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
ImPlot::PlotLine("##data", xs, ys, count);
ImPlot::EndPlot();
}
}
void line_plot(const char* title, const double* xs, const double* ys, int count) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
ImPlot::PlotLine("##data", xs, ys, count);
ImPlot::EndPlot();
}
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
// Renders a 2D line plot using ImPlot.
// Call within an ImGui frame (inside fn::run_app render callback).
void line_plot(const char* title, const float* xs, const float* ys, int count);
// Overload with double precision.
void line_plot(const char* title, const double* xs, const double* ys, int count);
+40
View File
@@ -0,0 +1,40 @@
---
name: line_plot
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void line_plot(const char* title, const float* xs, const float* ys, int count)"
description: "Renderiza un grafico de lineas 2D usando ImPlot dentro de un frame ImGui"
tags: [implot, chart, visualization, gpu, line]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/line_plot.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del grafico, se muestra como header del plot"
- name: xs
desc: "Array de coordenadas X"
- name: ys
desc: "Array de coordenadas Y"
- name: count
desc: "Numero de puntos en los arrays xs/ys"
output: "Renderiza el grafico de lineas en el frame ImGui actual"
---
# line_plot
Wrapper atomico sobre `ImPlot::PlotLine`. Renderiza un grafico de lineas 2D con los datos proporcionados.
Debe llamarse dentro del render callback de `fn::run_app` (o cualquier contexto con un frame ImGui activo).
Soporta `float` y `double` precision.
+16
View File
@@ -0,0 +1,16 @@
#include "viz/scatter_plot.h"
#include "implot.h"
void scatter_plot(const char* title, const float* xs, const float* ys, int count) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
ImPlot::PlotScatter("##data", xs, ys, count);
ImPlot::EndPlot();
}
}
void scatter_plot(const char* title, const double* xs, const double* ys, int count) {
if (ImPlot::BeginPlot(title, ImVec2(-1, 0))) {
ImPlot::PlotScatter("##data", xs, ys, count);
ImPlot::EndPlot();
}
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
// Renders a scatter plot using ImPlot.
// Call within an ImGui frame.
void scatter_plot(const char* title, const float* xs, const float* ys, int count);
void scatter_plot(const char* title, const double* xs, const double* ys, int count);
+38
View File
@@ -0,0 +1,38 @@
---
name: scatter_plot
kind: component
lang: cpp
domain: viz
version: "1.0.0"
purity: pure
signature: "void scatter_plot(const char* title, const float* xs, const float* ys, int count)"
description: "Renderiza un grafico de dispersion usando ImPlot dentro de un frame ImGui"
tags: [implot, chart, visualization, gpu, scatter]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [implot]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/viz/scatter_plot.cpp"
framework: imgui
params:
- name: title
desc: "Titulo del grafico scatter"
- name: xs
desc: "Array de coordenadas X"
- name: ys
desc: "Array de coordenadas Y"
- name: count
desc: "Numero de puntos en los arrays xs/ys"
output: "Renderiza el grafico de dispersion en el frame ImGui actual"
---
# scatter_plot
Wrapper atomico sobre `ImPlot::PlotScatter`. Renderiza un grafico de dispersion 2D.
Debe llamarse dentro del render callback de `fn::run_app`.
+8
View File
@@ -0,0 +1,8 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER gcc)
set(CMAKE_CXX_COMPILER g++)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+17
View File
@@ -0,0 +1,17 @@
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Static link runtime so .exe is self-contained
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libgcc -static-libstdc++")
Vendored Submodule
+1
Submodule cpp/vendor/glfw added at b00e6a8a88
Vendored Submodule
+1
Submodule cpp/vendor/imgui added at f5f6ca07be
Vendored Submodule
+1
Submodule cpp/vendor/implot added at 524f9fcd48
Vendored Submodule
+1
Submodule cpp/vendor/tracy added at 00a069d608
-25
View File
@@ -1,25 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
+6 -6
View File
@@ -1,11 +1,11 @@
export const chartColors = [
'hsl(var(--chart-1, 220 70% 50%))',
'hsl(var(--chart-2, 160 60% 45%))',
'hsl(var(--chart-3, 30 80% 55%))',
'hsl(var(--chart-4, 280 65% 60%))',
'hsl(var(--chart-5, 340 75% 55%))',
'#3b82f6',
'#10b981',
'#f59e0b',
'#8b5cf6',
'#ef4444',
]
export function getChartColor(index: number): string {
return chartColors[index % chartColors.length]
return chartColors[index % chartColors.length]!
}
-40
View File
@@ -1,40 +0,0 @@
---
name: cn
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: pure
signature: "cn(...inputs: ClassValue[]): string"
description: "Combina clases CSS con clsx y resuelve conflictos Tailwind con tailwind-merge. Utilidad fundamental para composición de estilos."
tags: [css, tailwind, classname, merge, utility]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [clsx, tailwind-merge]
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"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/cn.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/lib/utils.ts"
---
## Ejemplo
```typescript
cn("px-4 py-2", "px-6") // "px-6 py-2" (tailwind-merge resuelve conflicto)
cn("text-red-500", false && "hidden") // "text-red-500" (clsx filtra falsy)
cn("rounded-lg", className) // composición con className externo
```
## Notas
Base de todo el sistema de estilos. Todos los componentes la usan para componer className.
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs))
}
@@ -1,69 +0,0 @@
---
name: generate_theme_css
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: pure
signature: "generateThemeCss(colors: Record<string, string>, selector?: string): string"
description: "Genera un bloque CSS con variables de tema a partir de un objeto de tokens. Convierte claves camelCase a kebab-case automaticamente. Pura — solo transforma datos, no accede al DOM."
tags: [theme, css, generator, pure]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/generate_theme_css.ts"
---
## Ejemplo
```typescript
import { generateThemeCss } from './generate_theme_css'
import { themeConfigToColors } from './theme_config_to_colors'
import { darkTheme } from '../ui/themes'
// Generar CSS para inyectar en <style>
const colors = themeConfigToColors(darkTheme)
const css = generateThemeCss(colors)
// Output:
// :root {
// --background: oklch(8% 0.015 260);
// --foreground: oklch(95% 0.01 260);
// --card: oklch(12% 0.015 260);
// --card-foreground: oklch(95% 0.01 260);
// ...
// }
// Inyectar en el documento
const style = document.createElement('style')
style.textContent = css
document.head.appendChild(style)
// Generar para selector especifico (dark mode)
const darkCss = generateThemeCss(colors, '.dark')
// Output: .dark { --background: oklch(...); ... }
// Generar para multiples selectores
const lightCss = generateThemeCss(lightColors, ':root')
const darkCss2 = generateThemeCss(darkColors, ':root.dark')
const combined = [lightCss, darkCss2].join('\n\n')
```
## Notas
Funcion pura — sin acceso al DOM, sin side effects. Util para SSR, generacion de archivos CSS estaticos, o pre-generar temas en build time.
La conversion camelCase → kebab-case es simple (reemplaza mayusculas con `-` + minuscula). No maneja casos especiales como `backgroundColor``background-color`; los tokens del registry ya usan nombres semanticos directos (`background`, `cardForeground`, etc.).
Compone naturalmente con `themeConfigToColors` del registry: `generateThemeCss(themeConfigToColors(config))`.
@@ -1,23 +0,0 @@
/**
* 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}`
}
+1 -1
View File
@@ -1,7 +1,7 @@
const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']
export function getSeriesColor(index: number, color?: string): string {
return color || defaultColors[index % defaultColors.length]
return color ?? defaultColors[index % defaultColors.length]!
}
export { defaultColors }
@@ -1,50 +0,0 @@
---
name: get_theme_tokens
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: impure
signature: "getThemeTokens(): ThemeTokens"
description: "Lee todas las CSS variables de tema del documento y devuelve un objeto tipado con los valores computados desde :root. Util para pasar colores a APIs que no entienden CSS variables (canvas, sigma.js, D3)."
tags: [theme, css, tokens, runtime, dom]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
output: "Objeto ThemeTokens con todas las variables CSS de tema resueltas (colores, tipografía, espaciado)"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/get_theme_tokens.ts"
---
## Ejemplo
```typescript
import { getThemeTokens } from './get_theme_tokens'
const tokens = getThemeTokens()
// Pasar colores a sigma.js (que no soporta CSS variables)
const sigmaSettings = {
defaultNodeColor: tokens.primary,
defaultEdgeColor: tokens.muted,
labelColor: { color: tokens.foreground },
}
// Pasar colores a un canvas 2D
const ctx = canvas.getContext('2d')
ctx.fillStyle = tokens.background
ctx.strokeStyle = tokens.border
```
## Notas
Impura — accede a `document.documentElement` y `getComputedStyle`. Solo disponible en browser.
Los valores retornados son los valores sin procesar de las CSS variables (ej: `oklch(8% 0.015 260)`). Para obtener valores RGB computed (necesarios para algunas APIs), usar `getComputedColor`.
Funciona con cualquier tema activo: el resultado cambia automaticamente cuando se cambia el tema via `applyTheme`.
@@ -1,59 +0,0 @@
/** Tokens de tema leidos de las CSS variables activas en :root. */
export interface ThemeTokens {
background: string
foreground: string
card: string
cardForeground: string
popover: string
popoverForeground: string
primary: string
primaryForeground: string
secondary: string
secondaryForeground: string
muted: string
mutedForeground: string
accent: string
accentForeground: string
destructive: string
destructiveForeground: string
success: string
successForeground: string
border: string
input: string
ring: string
}
/**
* Lee todas las CSS variables de tema del documento y devuelve un objeto
* tipado con los valores computados desde :root.
*
* Util para pasar colores a APIs que no entienden CSS variables
* (canvas, sigma.js, D3, etc.).
*/
export function getThemeTokens(): ThemeTokens {
const style = getComputedStyle(document.documentElement)
const get = (name: string) => style.getPropertyValue(`--${name}`).trim()
return {
background: get('background'),
foreground: get('foreground'),
card: get('card'),
cardForeground: get('card-foreground'),
popover: get('popover'),
popoverForeground: get('popover-foreground'),
primary: get('primary'),
primaryForeground: get('primary-foreground'),
secondary: get('secondary'),
secondaryForeground: get('secondary-foreground'),
muted: get('muted'),
mutedForeground: get('muted-foreground'),
accent: get('accent'),
accentForeground: get('accent-foreground'),
destructive: get('destructive'),
destructiveForeground: get('destructive-foreground'),
success: get('success'),
successForeground: get('success-foreground'),
border: get('border'),
input: get('input'),
ring: get('ring'),
}
}
@@ -1,41 +0,0 @@
---
name: theme_config_to_colors
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: pure
signature: "themeConfigToColors(config: ThemeConfig): ThemeColors"
description: "Convierte un ThemeConfig completo a ThemeColors plano para inyectar como CSS variables. Mapea tokens semánticos a variables CSS."
tags: [theme, colors, css-variables, conversion]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/core/theme_config_to_colors.ts"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/themes/types.ts"
---
## Ejemplo
```typescript
const colors = themeConfigToColors(darkThemeConfig)
// { background: '...', foreground: '...', primary: '...', ... }
```
## Notas
Puente entre el sistema de temas estructurado (ThemeConfig) y el sistema plano de CSS variables que consumen los componentes.
Depende de los tipos ThemeConfig y ThemeColors definidos en `frontend/types/ui/theme_config.ts`. El tipo aún no está indexado en la BD (pendiente añadir theme_config.md para que fn index lo registre).
@@ -1,49 +0,0 @@
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,
}
}
+8 -8
View File
@@ -6,14 +6,14 @@ domain: ui
version: "1.0.0"
purity: impure
signature: "Accordion(props: AccordionProps): JSX.Element"
description: "Secciones colapsables con animaciones. Base-UI Collapsible primitive. Composable: AccordionItem + AccordionTrigger + AccordionContent."
tags: [accordion, collapsible, component, ui, interactive, base-ui]
uses_functions: [cn_ts_core]
description: "Secciones colapsables con animaciones. Mantine Accordion. Composable: AccordionItem + AccordionTrigger + AccordionContent."
tags: [accordion, collapsible, component, ui, interactive, mantine]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react/collapsible", "lucide-react"]
imports: ["@mantine/core"]
output: "Componente Accordion que renderiza secciones colapsables con soporte para múltiples items abiertos simultáneamente"
tested: false
tests: []
@@ -33,14 +33,14 @@ variant: []
## Ejemplo
```tsx
<Accordion>
<AccordionItem defaultOpen>
<Accordion defaultValue="section-1">
<AccordionItem value="section-1">
<AccordionTrigger>Seccion 1</AccordionTrigger>
<AccordionContent>
Contenido de la primera seccion.
</AccordionContent>
</AccordionItem>
<AccordionItem>
<AccordionItem value="section-2">
<AccordionTrigger>Seccion 2</AccordionTrigger>
<AccordionContent>
Contenido de la segunda seccion.
@@ -51,4 +51,4 @@ variant: []
## Notas
Cada AccordionItem es un Collapsible independiente — permite multiples items abiertos simultaneamente. Para exclusividad (solo uno abierto), manejar el estado externamente. El chevron rota 180 grados con [data-open]. Exports: Accordion, AccordionItem, AccordionTrigger, AccordionContent.
Usa Mantine Accordion nativo. Soporta type single (default) y multiple para multiples items abiertos. El chevron se maneja automaticamente por Mantine. AccordionItem requiere prop value unico. Exports: Accordion, AccordionItem, AccordionTrigger, AccordionContent.
+48 -39
View File
@@ -1,7 +1,5 @@
import * as React from "react"
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "../core/cn"
import { Accordion as MantineAccordion } from "@mantine/core"
interface AccordionItem {
value: string
@@ -19,61 +17,72 @@ interface AccordionProps {
children?: React.ReactNode
}
function Accordion({ className, children, ...props }: React.ComponentProps<"div"> & AccordionProps) {
function Accordion({ className, type, defaultValue, children }: AccordionProps) {
if (type === "multiple") {
return (
<MantineAccordion
multiple
data-slot="accordion"
className={className}
defaultValue={Array.isArray(defaultValue) ? defaultValue : undefined}
>
{children}
</MantineAccordion>
)
}
return (
<div data-slot="accordion" className={cn("divide-y divide-border", className)} {...props}>
<MantineAccordion
data-slot="accordion"
className={className}
defaultValue={typeof defaultValue === "string" ? defaultValue : undefined}
>
{children}
</div>
</MantineAccordion>
)
}
interface AccordionItemProps extends CollapsiblePrimitive.Root.Props {
interface AccordionItemProps {
value: string
className?: string
children?: React.ReactNode
disabled?: boolean
}
function AccordionItem({ className, ...props }: AccordionItemProps) {
function AccordionItem({ className, value, children, ...props }: AccordionItemProps) {
return (
<CollapsiblePrimitive.Root
<MantineAccordion.Item
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
)}
value={value}
className={className}
{...props}
>
{children}
<ChevronDownIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</CollapsiblePrimitive.Trigger>
</MantineAccordion.Item>
)
}
function AccordionContent({ className, children, ...props }: CollapsiblePrimitive.Panel.Props) {
function AccordionTrigger({ className, children, ...props }: { className?: string; children?: React.ReactNode }) {
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
)}
<MantineAccordion.Control
data-slot="accordion-trigger"
className={className}
{...props}
>
<div className="pb-4">{children}</div>
</CollapsiblePrimitive.Panel>
{children}
</MantineAccordion.Control>
)
}
function AccordionContent({ className, children, ...props }: { className?: string; children?: React.ReactNode }) {
return (
<MantineAccordion.Panel
data-slot="accordion-content"
className={className}
{...props}
>
{children}
</MantineAccordion.Panel>
)
}
+77
View File
@@ -0,0 +1,77 @@
---
name: action_icon
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "FnActionIcon(props: FnActionIconProps): JSX.Element"
description: "Boton de icono con variantes, loading y tooltip opcional. Wrapper sobre Mantine ActionIcon."
tags: [mantine, button, icon, action, tooltip, component, ui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@mantine/core"]
framework: react
props:
- name: icon
type: "ReactNode"
required: true
description: "Icono a renderizar dentro del boton"
- name: variant
type: "'filled' | 'light' | 'outline' | 'transparent' | 'default' | 'subtle'"
required: false
description: "Variante visual del boton, default 'default'"
- name: size
type: "MantineSize | number"
required: false
description: "Tamano del boton"
- name: color
type: "MantineColor"
required: false
description: "Color del boton"
- name: onClick
type: "MouseEventHandler"
required: false
description: "Callback al hacer click"
- name: loading
type: "boolean"
required: false
description: "Muestra spinner de carga"
- name: disabled
type: "boolean"
required: false
description: "Deshabilita el boton"
- name: tooltip
type: "string"
required: false
description: "Si se provee, envuelve el boton en un Tooltip"
output: "Boton de icono con tooltip opcional, estados loading/disabled y multiples variantes"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/action_icon.tsx"
emits: []
has_state: false
variant: [filled, light, outline, transparent, default, subtle]
---
## Ejemplo
```tsx
import { FnActionIcon } from '@fn_library'
import { IconSettings } from '@tabler/icons-react'
<FnActionIcon
icon={<IconSettings size={18} />}
tooltip="Configuracion"
variant="light"
onClick={() => openSettings()}
/>
```
## Notas
Wrapper sobre Mantine `ActionIcon`. Si se provee `tooltip`, envuelve automaticamente en Mantine `Tooltip`. Compatible con iconos de `@tabler/icons-react`.
+47
View File
@@ -0,0 +1,47 @@
import * as React from 'react'
import { ActionIcon, Tooltip } from '@mantine/core'
import type { MantineSize, MantineColor } from '@mantine/core'
interface FnActionIconProps {
icon: React.ReactNode
variant?: 'filled' | 'light' | 'outline' | 'transparent' | 'default' | 'subtle'
size?: MantineSize | number
color?: MantineColor
onClick?: React.MouseEventHandler<HTMLButtonElement>
loading?: boolean
disabled?: boolean
tooltip?: string
}
function FnActionIcon({
icon,
variant = 'default',
size = 'md',
color,
onClick,
loading,
disabled,
tooltip,
}: FnActionIconProps) {
const button = (
<ActionIcon
variant={variant}
size={size}
color={color}
onClick={onClick}
loading={loading}
disabled={disabled}
>
{icon}
</ActionIcon>
)
if (tooltip) {
return <Tooltip label={tooltip}>{button}</Tooltip>
}
return button
}
export { FnActionIcon }
export type { FnActionIconProps }
+7 -7
View File
@@ -6,15 +6,15 @@ domain: ui
version: "1.0.0"
purity: impure
signature: "Alert(props: { variant?: 'default' | 'destructive' }): JSX.Element"
description: "Alerta accesible con variantes default y destructive. Sistema de slots para título, descripción, icono y acción."
tags: [alert, feedback, component, ui, notification]
uses_functions: [cn_ts_core]
description: "Alerta accesible con variantes default y destructive. Mantine Alert con slots para título, descripción y acción."
tags: [alert, feedback, component, ui, notification, mantine]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react, class-variance-authority]
output: "Componente Alert que renderiza una alerta accesible con slots para título, descripción, icono y acción"
imports: ["@mantine/core", react]
output: "Componente Alert que renderiza una alerta accesible via Mantine Alert con slots para título, descripción y acción"
tested: false
tests: []
test_file_path: ""
@@ -46,5 +46,5 @@ source_file: "frontend/src/components/ui/alert.tsx"
## Notas
Exporta 4 subcomponentes composables via data-slot: Alert, AlertTitle, AlertDescription, AlertAction.
El icono SVG se posiciona automáticamente en grid cuando es hijo directo de Alert.
AlertAction se posiciona absolute top-right para acciones secundarias (ej: botón cerrar).
AlertAction se posiciona absolute top-right para acciones secundarias (ej: boton cerrar).
alertVariants se exporta como objeto vacio por compatibilidad (Mantine gestiona variantes via color prop).
+59 -23
View File
@@ -1,34 +1,70 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
import * as React from 'react'
import { Alert as MantineAlert, Box, Text } from '@mantine/core'
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" },
}
)
type AlertVariant = 'default' | 'destructive'
function Alert({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return <div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
const variantColorMap: Record<AlertVariant, string | undefined> = {
default: undefined,
destructive: 'red',
}
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 Alert({
className,
variant = 'default',
children,
...props
}: React.ComponentProps<'div'> & { variant?: AlertVariant }) {
return (
<MantineAlert
data-slot="alert"
color={variantColorMap[variant]}
radius="md"
variant="light"
className={className}
{...props}
>
{children}
</MantineAlert>
)
}
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 AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<Text
component="div"
data-slot="alert-title"
fw={500}
size="sm"
className={className}
{...props}
/>
)
}
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="alert-action" className={cn("absolute top-2 right-2", className)} {...props} />
function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<Text
component="div"
data-slot="alert-description"
size="sm"
c="dimmed"
className={className}
{...props}
/>
)
}
function AlertAction({ className, style, ...props }: React.ComponentProps<'div'>) {
return (
<Box
data-slot="alert-action"
style={{ position: 'absolute', top: 'var(--mantine-spacing-xs)', right: 'var(--mantine-spacing-xs)', ...style }}
className={className}
{...props}
/>
)
}
const alertVariants = {} as const
export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants }
+2 -2
View File
@@ -8,12 +8,12 @@ purity: pure
signature: "analyticsPage(props: AnalyticsPageProps): ReactElement"
description: "Genera un dashboard de analytics completo con header, fila de KPIs con deltas y grid de charts configurables."
tags: [analytics, dashboard, kpi, charts, factory, composition, ui]
uses_functions: [cn_ts_core]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [react]
imports: [react, "@mantine/core"]
params:
- name: props
desc: "Configuración del dashboard: título, métricas con deltas, y lista de charts con span"
+40 -44
View File
@@ -1,5 +1,5 @@
import * as React from 'react'
import { cn } from '../core/cn'
import { Stack, Group, Title, Text, Paper, SimpleGrid } from '@mantine/core'
interface MetricConfig {
label: string
@@ -34,67 +34,63 @@ export function analyticsPage({
metrics,
charts,
actions,
className,
}: AnalyticsPageProps): React.ReactElement {
const metricCols = metrics.length <= 2 ? { base: 1, md: 2 } : metrics.length <= 3 ? { base: 1, md: 3 } : { base: 1, md: 2, lg: 4 }
return (
<div className={cn('space-y-6', className)}>
<Stack gap="lg">
{/* 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">
<Group justify="space-between" pb="md" style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}>
<Stack gap={4}>
<Title order={2}>{title}</Title>
{subtitle && <Text size="sm" c="dimmed">{subtitle}</Text>}
</Stack>
<Group gap="xs">
{dateRange}
{actions}
</div>
</div>
</Group>
</Group>
{/* 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'
)}>
<SimpleGrid cols={metricCols} spacing="md">
{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>
<Paper key={i} p="md" withBorder shadow="xs">
<Text size="sm" c="dimmed">{metric.label}</Text>
<Group mt="xs" justify="space-between" align="flex-end" gap="md">
<Stack gap={4}>
<Text fz={30} fw={700} lh={1}>{metric.value}</Text>
{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>
<Text
size="sm"
fw={500}
c={metric.delta.value === 0 ? 'dimmed' : metric.delta.isPositive ? 'green' : 'red'}
>
{metric.delta.value > 0 ? '+' : ''}{metric.delta.value}%
</Text>
)}
</div>
</div>
</div>
</Stack>
</Group>
</Paper>
))}
</div>
</SimpleGrid>
{/* Charts Grid */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md">
{charts.map((chart) => (
<div
<Paper
key={chart.id}
className={cn(
'rounded-xl border bg-card p-4 text-card-foreground shadow-sm',
chart.span === 2 && 'lg:col-span-2'
)}
p="md"
withBorder
shadow="xs"
radius="md"
style={chart.span === 2 ? { gridColumn: 'span 2' } : undefined}
>
<h3 className="mb-3 text-sm font-medium text-muted-foreground">{chart.title}</h3>
<Text size="sm" fw={500} c="dimmed" mb="sm">{chart.title}</Text>
{chart.content}
</div>
</Paper>
))}
</div>
</div>
</SimpleGrid>
</Stack>
)
}
+65
View File
@@ -0,0 +1,65 @@
---
name: app_shell
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "FnAppShell(props: FnAppShellProps): JSX.Element"
description: "Layout shell con header, navbar colapsable y area principal. Wrapper sobre Mantine AppShell."
tags: [mantine, layout, shell, navigation, component, ui]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@mantine/core"]
framework: react
props:
- name: header
type: "ReactNode"
required: false
description: "Contenido del header superior"
- name: navbar
type: "ReactNode"
required: false
description: "Contenido del sidebar de navegacion"
- name: navbarWidth
type: "number"
required: false
description: "Ancho del navbar en px, default 250"
- name: navbarCollapsed
type: "boolean"
required: false
description: "Si el navbar esta colapsado"
- name: children
type: "ReactNode"
required: true
description: "Contenido principal del area main"
output: "Layout de aplicacion con header fijo, sidebar colapsable y area de contenido principal"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/app_shell.tsx"
emits: []
has_state: false
variant: []
---
## Ejemplo
```tsx
import { FnAppShell } from '@fn_library'
<FnAppShell
header={<Group px="md">Logo</Group>}
navbar={<NavLinks />}
navbarCollapsed={collapsed}
>
<MainContent />
</FnAppShell>
```
## Notas
Wrapper sobre Mantine `AppShell`. El header tiene altura fija de 60px. El navbar colapsa tanto en mobile como en desktop cuando `navbarCollapsed` es true. El breakpoint de responsive es `sm`.
+33
View File
@@ -0,0 +1,33 @@
import * as React from 'react'
import { AppShell } from '@mantine/core'
interface FnAppShellProps {
header?: React.ReactNode
navbar?: React.ReactNode
navbarWidth?: number
navbarCollapsed?: boolean
children: React.ReactNode
}
function FnAppShell({
header,
navbar,
navbarWidth = 250,
navbarCollapsed = false,
children,
}: FnAppShellProps) {
return (
<AppShell
header={header ? { height: 60 } : undefined}
navbar={navbar ? { width: navbarWidth, breakpoint: 'sm', collapsed: { mobile: navbarCollapsed, desktop: navbarCollapsed } } : undefined}
padding="md"
>
{header && <AppShell.Header>{header}</AppShell.Header>}
{navbar && <AppShell.Navbar p="md">{navbar}</AppShell.Navbar>}
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
)
}
export { FnAppShell }
export type { FnAppShellProps }
-44
View File
@@ -1,44 +0,0 @@
---
name: apply_theme
kind: function
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "applyTheme(theme: Theme): void"
description: "Inyecta un tema como CSS variables en document.documentElement. Maneja clase dark automáticamente. Mapea 40 tokens semánticos."
tags: [theme, css-variables, apply, runtime, ui]
uses_functions: []
uses_types: [ThemeConfig_ts_ui]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
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"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/apply_theme.tsx"
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/hooks/use-theme.tsx"
---
## Ejemplo
```typescript
import { applyTheme } from './apply_theme'
applyTheme({
name: 'dark',
label: 'Oscuro',
colors: themeConfigToColors(darkThemeConfig)
})
```
## Notas
Función impura (modifica el DOM). Mapea cada key de ThemeColors a una CSS variable. Temas oscuros (dark, midnight, sunset) añaden clase `dark` al root.
-111
View File
@@ -1,111 +0,0 @@
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 }
+5 -5
View File
@@ -6,15 +6,15 @@ 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]
description: "Gráfico de área @mantine/charts con gradientes automáticos, multi-series, stacking y tooltips."
tags: [chart, area, visualization, mantine, gradient, component, ui]
uses_functions: [chart_container_ts_ui]
uses_types: [ChartSeries_ts_ui]
returns: []
returns_optional: false
error_type: ""
imports: [recharts]
output: "Componente JSX que renderiza un gráfico de área con gradientes, multi-series y tooltips temáticos"
imports: ["@mantine/charts", "@mantine/core"]
output: "Componente JSX que renderiza un gráfico de área con gradientes, multi-series y tooltips"
tested: false
tests: []
test_file_path: ""
+29 -37
View File
@@ -1,9 +1,6 @@
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 }
import { AreaChart as MantineAreaChart } from '@mantine/charts'
import { Paper } from '@mantine/core'
import { type Series, getSeriesColor } from './chart_container'
interface AreaChartProps {
data: Record<string, unknown>[]
@@ -11,11 +8,10 @@ interface AreaChartProps {
yKey?: string
series?: Series[]
stacked?: boolean
gradient?: GradientConfig | boolean
gradient?: boolean
showGrid?: boolean
showLegend?: boolean
height?: number | string
className?: string
height?: number
xAxisFormatter?: (value: unknown) => string
yAxisFormatter?: (value: unknown) => string
valueFormatter?: (value: number) => string
@@ -23,40 +19,36 @@ interface AreaChartProps {
function AreaChartComponent({
data, xKey, yKey, series, stacked = false, gradient = true, showGrid = true,
showLegend = false, height = 300, className, xAxisFormatter, yAxisFormatter,
showLegend = false, height = 300, 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
const chartSeries = series
? series.map((s, i) => ({ name: s.key, label: s.name, color: getSeriesColor(i, s.color) }))
: yKey ? [{ name: yKey, label: yKey, color: getSeriesColor(0) }] : []
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>
<Paper p="md">
<MantineAreaChart
h={height}
data={data}
dataKey={xKey}
series={chartSeries}
type={stacked ? 'stacked' : 'default'}
curveType="monotone"
withGradient={gradient}
gridAxis={showGrid ? 'xy' : 'none'}
withLegend={showLegend}
withTooltip
valueFormatter={valueFormatter}
xAxisProps={xAxisFormatter ? { tickFormatter: xAxisFormatter } : undefined}
yAxisProps={yAxisFormatter ? { tickFormatter: yAxisFormatter } : undefined}
/>
</Paper>
)
}
/** @deprecated Gradient is handled by Mantine's withGradient prop */
type GradientConfig = { from: string; to: string }
export const AreaChart = AreaChartComponent
export type { AreaChartProps, GradientConfig }
+101
View File
@@ -0,0 +1,101 @@
---
name: auth_form
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "AuthForm(config: AuthFormConfig): ReactElement"
description: "Genera página de autenticación con toggle login/register, social buttons opcionales, campos extra en registro y validación. Basado en Mantine AuthenticationForm."
tags: [auth, login, register, form, page, ui, mantine, toggle]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@mantine/core", "@mantine/hooks"]
has_state: true
framework: react
emits: [onSubmit]
params:
- name: title
desc: "Título principal que aparece en la cabecera del formulario (default: 'Welcome')"
- name: socialButtons
desc: "Lista de botones de login social, cada uno con label, icono opcional y callback onClick"
- name: extraFields
desc: "Campos de texto adicionales que se muestran únicamente en el modo registro (ej: nombre, empresa)"
- name: onSubmit
desc: "Callback invocado al enviar el formulario con type ('login'|'register'), email, password y valores de extraFields"
- name: defaultType
desc: "Modo inicial del formulario: 'login' (default) o 'register'"
- name: paperProps
desc: "Props de Mantine Paper para personalizar el contenedor (shadow, radius, p, etc.)"
output: "Página de autenticación completa con toggle login/register, campos email/password, botones sociales opcionales y campo de términos en registro"
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/auth_form.tsx"
source_repo: ""
source_license: ""
source_file: ""
---
## Ejemplo
### Config mínima (solo login)
```tsx
import { AuthForm } from '@fn_library/auth_form'
function LoginPage() {
return (
<AuthForm
title="Acceder"
onSubmit={({ type, email, password }) => {
console.log(type, email, password)
}}
/>
)
}
```
### Con social buttons y campos extra en registro
```tsx
import { AuthForm } from '@fn_library/auth_form'
import { IconBrandGoogle, IconBrandGithub } from '@tabler/icons-react'
function AuthPage() {
return (
<AuthForm
title="fn_registry"
defaultType="register"
socialButtons={[
{ label: 'Google', icon: <IconBrandGoogle size={16} />, onClick: () => signInWithGoogle() },
{ label: 'GitHub', icon: <IconBrandGithub size={16} />, onClick: () => signInWithGitHub() },
]}
extraFields={[
{ name: 'name', label: 'Nombre completo', placeholder: 'Lucas García', required: true },
{ name: 'company', label: 'Empresa', placeholder: 'Acme Corp' },
]}
onSubmit={({ type, email, password, name, company }) => {
if (type === 'register') {
registerUser({ email, password, name, company })
} else {
loginUser({ email, password })
}
}}
/>
)
}
```
## Notas
Función con estado interno (useToggle, useState de @mantine/hooks). Gestiona el toggle entre login y register sin prop externa — el padre solo recibe el valor final via onSubmit.type.
Los `extraFields` solo se renderizan en modo register. El campo de términos y condiciones (Checkbox) también es exclusivo del registro.
Los `socialButtons` se renderizan con un Divider "O continúa con email" cuando están presentes. Sin socialButtons el Divider no aparece.
El campo `password` usa PasswordInput de Mantine, que incluye el toggle de visibilidad integrado.
+181
View File
@@ -0,0 +1,181 @@
import * as React from 'react'
import {
Anchor,
Button,
Checkbox,
Divider,
Group,
Paper,
PasswordInput,
Stack,
Text,
TextInput,
Title,
Container,
type PaperProps,
} from '@mantine/core'
import { useToggle, upperFirst } from '@mantine/hooks'
interface SocialButtonConfig {
label: string
icon?: React.ReactNode
onClick?: () => void
}
interface ExtraFieldConfig {
name: string
label: string
placeholder?: string
required?: boolean
}
interface AuthFormSubmitValues {
type: 'login' | 'register'
email: string
password: string
[key: string]: unknown
}
interface AuthFormConfig {
/** Título principal de la página */
title?: string
/** Botones de autenticación social opcionales */
socialButtons?: SocialButtonConfig[]
/** Campos adicionales que se muestran solo en el modo registro */
extraFields?: ExtraFieldConfig[]
/** Callback invocado al enviar el formulario */
onSubmit?: (values: AuthFormSubmitValues) => void
/** Modo inicial: 'login' (default) o 'register' */
defaultType?: 'login' | 'register'
/** Props adicionales para el Paper contenedor */
paperProps?: PaperProps
}
function AuthForm({
title = 'Welcome',
socialButtons = [],
extraFields = [],
onSubmit,
defaultType = 'login',
paperProps,
}: AuthFormConfig): React.ReactElement {
const [type, toggle] = useToggle<'login' | 'register'>([
defaultType,
defaultType === 'login' ? 'register' : 'login',
])
const [email, setEmail] = React.useState('')
const [password, setPassword] = React.useState('')
const [terms, setTerms] = React.useState(true)
const [extraValues, setExtraValues] = React.useState<Record<string, string>>({})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
onSubmit?.({ type, email, password, ...extraValues })
}
const handleExtraChange = (name: string, value: string) => {
setExtraValues((prev) => ({ ...prev, [name]: value }))
}
return (
<Container size={420} py={40}>
<Paper radius="md" p="lg" withBorder {...paperProps}>
<Title order={2} ta="center" mb="md">
{title}
</Title>
{socialButtons.length > 0 && (
<>
<Group grow mb="md" gap="xs">
{socialButtons.map((btn) => (
<Button
key={btn.label}
variant="default"
radius="xl"
leftSection={btn.icon}
onClick={btn.onClick}
>
{btn.label}
</Button>
))}
</Group>
<Divider label="O continúa con email" labelPosition="center" my="lg" />
</>
)}
<form onSubmit={handleSubmit}>
<Stack gap="sm">
{type === 'register' &&
extraFields.map((field) => (
<TextInput
key={field.name}
label={field.label}
placeholder={field.placeholder}
required={field.required}
value={extraValues[field.name] ?? ''}
onChange={(e) => handleExtraChange(field.name, e.currentTarget.value)}
radius="md"
/>
))}
<TextInput
required
label="Email"
placeholder="tu@email.com"
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
radius="md"
/>
<PasswordInput
required
label="Contraseña"
placeholder="Tu contraseña"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
radius="md"
/>
{type === 'register' && (
<Checkbox
label="Acepto los términos y condiciones"
checked={terms}
onChange={(e) => setTerms(e.currentTarget.checked)}
/>
)}
</Stack>
<Group justify="space-between" mt="xl">
<Anchor
component="button"
type="button"
c="dimmed"
size="xs"
onClick={() => toggle()}
>
{type === 'register'
? '¿Ya tienes cuenta? Inicia sesión'
: '¿No tienes cuenta? Regístrate'}
</Anchor>
<Button type="submit" radius="xl">
{upperFirst(type)}
</Button>
</Group>
</form>
{type === 'register' && (
<Text c="dimmed" size="xs" ta="center" mt="md">
Al registrarte aceptas nuestra{' '}
<Anchor size="xs" href="#">
política de privacidad
</Anchor>
.
</Text>
)}
</Paper>
</Container>
)
}
export { AuthForm }
export type { AuthFormConfig, AuthFormSubmitValues, SocialButtonConfig, ExtraFieldConfig }
+136
View File
@@ -0,0 +1,136 @@
---
name: autocomplete
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "Autocomplete(props: AutocompleteProps): JSX.Element"
description: "Input con sugerencias de autocompletado. Permite valores libres a diferencia de Select. Wrapper sobre Mantine Autocomplete."
tags: [autocomplete, input, form, suggestions, component, ui, interactive, mantine]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["@mantine/core"]
output: "Componente Autocomplete que renderiza input con dropdown de sugerencias filtradas"
props:
- name: data
type: "string[] | { value: string; label?: string; group?: string }[]"
required: true
description: "Lista de opciones a mostrar en el dropdown"
- name: value
type: "string"
required: false
description: "Valor controlado del input"
- name: onChange
type: "(value: string) => void"
required: false
description: "Callback al cambiar el valor del input"
- name: label
type: "string"
required: false
description: "Etiqueta visible encima del input"
- name: placeholder
type: "string"
required: false
description: "Texto placeholder cuando el input está vacío"
- name: clearable
type: "boolean"
required: false
description: "Muestra botón para limpiar el valor"
- name: loading
type: "boolean"
required: false
description: "Muestra spinner de carga en el input"
- name: disabled
type: "boolean"
required: false
description: "Deshabilita el input"
- name: limit
type: "number"
required: false
description: "Número máximo de sugerencias a mostrar en el dropdown"
- name: size
type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'"
required: false
description: "Tamaño visual del input"
emits: [onChange]
has_state: true
framework: react
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/autocomplete.tsx"
---
## Ejemplo
```tsx
import { Autocomplete } from '@fn_library/autocomplete'
// Básico — lista de strings
function BasicAutocomplete() {
return (
<Autocomplete
label="País"
placeholder="Escribe para buscar..."
data={['Argentina', 'Brasil', 'Chile', 'Colombia', 'Uruguay']}
/>
)
}
// Con grupos
function GroupedAutocomplete() {
return (
<Autocomplete
label="Ciudad"
placeholder="Selecciona una ciudad"
data={[
{ value: 'Buenos Aires', group: 'Argentina' },
{ value: 'Rosario', group: 'Argentina' },
{ value: 'São Paulo', group: 'Brasil' },
{ value: 'Río de Janeiro', group: 'Brasil' },
]}
limit={5}
/>
)
}
// Con loading y clearable (búsqueda asíncrona)
function AsyncAutocomplete() {
const [value, setValue] = useState('')
const [data, setData] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const handleChange = async (val: string) => {
setValue(val)
if (val.length < 2) return
setLoading(true)
const results = await fetchSuggestions(val)
setData(results)
setLoading(false)
}
return (
<Autocomplete
label="Búsqueda"
placeholder="Escribe al menos 2 caracteres..."
value={value}
onChange={handleChange}
data={data}
loading={loading}
clearable
/>
)
}
```
## Notas
A diferencia de `Select`, `Autocomplete` permite que el usuario ingrese cualquier valor libre, no solo los de la lista. Ideal para búsquedas con sugerencias donde el valor final puede no estar en el dataset.
Cuando `data` contiene objetos con `group`, el dropdown agrupa visualmente las opciones bajo el encabezado del grupo.
El prop `limit` controla cuántas sugerencias se muestran simultáneamente (por defecto Mantine muestra todas). Útil para datasets grandes o búsquedas asíncronas donde se quiere limitar el ruido visual.
+10
View File
@@ -0,0 +1,10 @@
import { Autocomplete as MantineAutocomplete, type AutocompleteProps as MantineAutcompleteProps } from '@mantine/core'
interface AutocompleteProps extends MantineAutcompleteProps {}
function Autocomplete(props: AutocompleteProps) {
return <MantineAutocomplete {...props} />
}
export { Autocomplete }
export type { AutocompleteProps }
+6 -6
View File
@@ -6,14 +6,14 @@ 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]
description: "Imagen de usuario circular con fallback a iniciales generadas automaticamente. 5 tamaños via Mantine Avatar."
tags: [avatar, user, image, component, ui, mantine]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["class-variance-authority"]
imports: ["@mantine/core"]
output: "Componente Avatar que renderiza imagen de usuario circular con fallback a iniciales generadas"
tested: false
tests: []
@@ -45,7 +45,7 @@ props:
required: false
description: "Clases CSS adicionales"
emits: []
has_state: true
has_state: false
framework: react
variant: [xs, sm, md, lg, xl]
---
@@ -68,4 +68,4 @@ variant: [xs, sm, md, lg, xl]
## Notas
Usa estado interno para manejar errores de carga de imagen (onError). La funcion getInitials extrae 2 iniciales del nombre completo (primera y ultima palabra). Si solo hay una palabra, toma los 2 primeros caracteres. Usa forwardRef para compatibilidad con wrappers.
Usa Mantine Avatar que maneja errores de carga de imagen nativamente. 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.
+33 -45
View File
@@ -1,69 +1,57 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
import * as React from 'react'
import { Avatar as MantineAvatar } from '@mantine/core'
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" },
}
)
type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
interface AvatarProps
extends React.ComponentPropsWithoutRef<"span">,
VariantProps<typeof avatarVariants> {
const sizeMap: Record<AvatarSize, string> = {
xs: 'sm',
sm: 'sm',
md: 'md',
lg: 'lg',
xl: 'xl',
}
interface AvatarProps extends React.ComponentPropsWithoutRef<'div'> {
src?: string
alt?: string
fallback?: string
initials?: string
size?: AvatarSize
}
function getInitials(name?: string): string {
if (!name) return "?"
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 first = parts[0] ?? ''
const last = parts[parts.length - 1] ?? ''
if (parts.length === 1) return first.slice(0, 2).toUpperCase()
return ((first[0] ?? '') + (last[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
/** Kept for backwards compatibility */
const avatarVariants = sizeMap
const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
({ className, size = 'md', src, alt, fallback, initials, ...props }, ref) => {
const displayInitials = initials ?? getInitials(fallback ?? alt)
return (
<span
<MantineAvatar
ref={ref}
data-slot="avatar"
className={cn(avatarVariants({ size }), className)}
src={src}
alt={alt ?? ''}
size={sizeMap[size]}
radius="xl"
className={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>
{displayInitials}
</MantineAvatar>
)
}
)
Avatar.displayName = "Avatar"
Avatar.displayName = 'Avatar'
export { Avatar, avatarVariants }
export type { AvatarProps }
export type { AvatarProps, AvatarSize }
+5 -5
View File
@@ -6,14 +6,14 @@ domain: ui
version: "1.0.0"
purity: impure
signature: "Badge(props: BadgeProps & VariantProps<typeof badgeVariants>): JSX.Element"
description: "Badge con 10 variantes semánticas (default, secondary, destructive, outline, ghost, link, success, warning, error, info) y 2 tamaños."
tags: [badge, status, component, ui, indicator]
uses_functions: [cn_ts_core]
description: "Badge con 10 variantes semánticas (default, secondary, destructive, outline, ghost, link, success, warning, error, info) y 2 tamaños. Mantine Badge."
tags: [badge, status, component, ui, indicator, mantine]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["class-variance-authority"]
imports: ["@mantine/core"]
output: "Componente Badge que renderiza un indicador visual con 10 variantes semánticas de estado"
tested: false
tests: []
@@ -46,4 +46,4 @@ source_file: "frontend/src/components/ui/badge.tsx"
## Notas
Versión simplificada que usa span nativo en lugar de useRender de Base-UI. Mantiene todas las variantes y la composibilidad con cn().
Usa Mantine Badge internamente. Las 10 variantes se mapean a combinaciones de variant+color de Mantine (filled, light, outline, subtle, transparent).
+37 -35
View File
@@ -1,45 +1,47 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
import * as React from 'react'
import { Badge as MantineBadge } from '@mantine/core'
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",
},
}
)
type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | 'success' | 'warning' | 'error' | 'info'
type BadgeSize = 'default' | 'sm'
interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
const variantMap: Record<BadgeVariant, { variant: string; color?: string }> = {
default: { variant: 'filled' },
secondary: { variant: 'light' },
destructive: { variant: 'light', color: 'red' },
outline: { variant: 'outline' },
ghost: { variant: 'subtle' },
link: { variant: 'transparent' },
success: { variant: 'light', color: 'green' },
warning: { variant: 'light', color: 'yellow' },
error: { variant: 'light', color: 'red' },
info: { variant: 'light', color: 'blue' },
}
/** Kept for backwards compatibility */
const badgeVariants = variantMap
interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: BadgeVariant
size?: BadgeSize
}
function Badge({ className, variant = 'default', size = 'default', children, ...props }: BadgeProps) {
const mv = variantMap[variant]
function Badge({ className, variant = "default", size = "default", ...props }: BadgeProps) {
return (
<span
<MantineBadge
data-slot="badge"
className={cn(badgeVariants({ variant, size }), className)}
variant={mv.variant}
color={mv.color}
size={size === 'sm' ? 'xs' : 'sm'}
radius="xl"
className={className}
{...props}
/>
>
{children}
</MantineBadge>
)
}
export { Badge, badgeVariants }
export type { BadgeProps, BadgeVariant, BadgeSize }
+6 -6
View File
@@ -6,15 +6,15 @@ domain: ui
version: "1.1.0"
purity: impure
signature: "BarChart(props: BarChartProps): JSX.Element"
description: "Gráfico de barras Recharts con multi-series, orientación horizontal/vertical, tooltips temáticos y bordes redondeados."
tags: [chart, bar, visualization, recharts, component, ui]
uses_functions: [cn_ts_core, chart_container_ts_ui, get_series_color_ts_core]
description: "Gráfico de barras @mantine/charts con multi-series, orientación horizontal/vertical y tooltips."
tags: [chart, bar, visualization, mantine, component, ui]
uses_functions: [chart_container_ts_ui]
uses_types: [ChartSeries_ts_ui]
returns: []
returns_optional: false
error_type: ""
imports: [recharts]
output: "Componente JSX que renderiza un gráfico de barras vertical u horizontal con multi-series y tooltips temáticos"
imports: ["@mantine/charts", "@mantine/core"]
output: "Componente JSX que renderiza un gráfico de barras vertical u horizontal con multi-series y tooltips"
tested: false
tests: []
test_file_path: ""
@@ -54,4 +54,4 @@ source_file: "frontend/src/components/ui/charts/bar-chart.tsx"
## Notas
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.
En modo `horizontal=true` se pasa `orientation="vertical"` a Mantine BarChart, que internamente intercambia los ejes.
+23 -29
View File
@@ -1,7 +1,6 @@
import {
BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
} from 'recharts'
import { ChartContainer, ChartTooltipContent, type Series, getSeriesColor } from './chart_container'
import { BarChart as MantineBarChart } from '@mantine/charts'
import { Paper } from '@mantine/core'
import { type Series, getSeriesColor } from './chart_container'
interface BarChartProps {
data: Record<string, unknown>[]
@@ -11,8 +10,7 @@ interface BarChartProps {
horizontal?: boolean
showGrid?: boolean
showLegend?: boolean
height?: number | string
className?: string
height?: number
xAxisFormatter?: (value: unknown) => string
yAxisFormatter?: (value: unknown) => string
valueFormatter?: (value: number) => string
@@ -20,32 +18,28 @@ interface BarChartProps {
function BarChartComponent({
data, xKey, yKey, series, horizontal = false, showGrid = true, showLegend = false,
height = 300, className, xAxisFormatter, yAxisFormatter, valueFormatter = (v) => v.toLocaleString(),
height = 300, 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) }] : []
const chartSeries = series
? series.map((s, i) => ({ name: s.key, label: s.name, color: getSeriesColor(i, s.color) }))
: yKey ? [{ name: yKey, label: yKey, color: 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>
<Paper p="md">
<MantineBarChart
h={height}
data={data}
dataKey={xKey}
series={chartSeries}
orientation={horizontal ? 'vertical' : 'horizontal'}
gridAxis={showGrid ? 'xy' : 'none'}
withLegend={showLegend}
withTooltip
valueFormatter={valueFormatter}
xAxisProps={xAxisFormatter ? { tickFormatter: xAxisFormatter } : undefined}
yAxisProps={yAxisFormatter ? { tickFormatter: yAxisFormatter } : undefined}
/>
</Paper>
)
}
+5 -5
View File
@@ -6,14 +6,14 @@ domain: ui
version: "1.0.0"
purity: impure
signature: "Breadcrumb(props: BreadcrumbProps): JSX.Element"
description: "Navegacion jerarquica con separadores, elipsis para paths largos y soporte para router links via asChild."
tags: [breadcrumb, navigation, component, ui]
uses_functions: [cn_ts_core]
description: "Navegacion jerarquica con separadores, elipsis para paths largos y soporte para router links via asChild. Mantine Anchor/Text."
tags: [breadcrumb, navigation, component, ui, mantine]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["lucide-react"]
imports: ["@mantine/core", "@tabler/icons-react"]
output: "Componente Breadcrumb que renderiza navegación jerárquica con separadores, elipsis y soporte para router links"
tested: false
tests: []
@@ -69,4 +69,4 @@ variant: []
## Notas
Exports: Breadcrumb (nav), BreadcrumbList (ol), BreadcrumbItem (li), BreadcrumbLink (a con asChild), BreadcrumbPage (span aria-current=page), BreadcrumbSeparator (ChevronRight por defecto, customizable), BreadcrumbEllipsis (MoreHorizontal). BreadcrumbLink acepta asChild para usar con Link de React Router o Next.js.
Exports: Breadcrumb (nav), BreadcrumbList (ol via Group), BreadcrumbItem (li via Group), BreadcrumbLink (Mantine Anchor con asChild), BreadcrumbPage (Text aria-current=page), BreadcrumbSeparator (IconChevronRight por defecto, customizable), BreadcrumbEllipsis (IconDots). BreadcrumbLink acepta asChild para usar con Link de React Router o Next.js. Usa Tabler icons en vez de lucide-react.
+34 -36
View File
@@ -1,28 +1,24 @@
import * as React from "react"
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
import { cn } from "../core/cn"
import { Anchor, Text, Box } from "@mantine/core"
import { IconChevronRight, IconDots } from "@tabler/icons-react"
function Breadcrumb({ ...props }: React.ComponentPropsWithoutRef<"nav">) {
return <nav data-slot="breadcrumb" aria-label="breadcrumb" {...props} />
function Breadcrumb({ children, ...props }: React.ComponentPropsWithoutRef<"nav">) {
return <nav data-slot="breadcrumb" aria-label="breadcrumb" {...props}>{children}</nav>
}
function BreadcrumbList({ className, ...props }: React.ComponentPropsWithoutRef<"ol">) {
function BreadcrumbList({ className, children, ...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}
/>
<ol data-slot="breadcrumb-list" style={{ listStyle: "none", display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, padding: 0, margin: 0 }} className={className} {...props}>
{children}
</ol>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentPropsWithoutRef<"li">) {
function BreadcrumbItem({ className, children, ...props }: React.ComponentPropsWithoutRef<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
<li data-slot="breadcrumb-item" style={{ display: "flex", alignItems: "center", gap: 8 }} className={className} {...props}>
{children}
</li>
)
}
@@ -35,31 +31,29 @@ function BreadcrumbLink({
}: 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">)}>
<Text data-slot="breadcrumb-link" component="span" size="sm" className={className} {...(props as React.ComponentPropsWithoutRef<"span">)}>
{children}
</span>
</Text>
)
}
return (
<a
data-slot="breadcrumb-link"
href={href}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
>
<Anchor data-slot="breadcrumb-link" href={href} size="sm" className={className} {...props}>
{children}
</a>
</Anchor>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
return (
<span
<Text
data-slot="breadcrumb-page"
component="span"
size="sm"
fw={500}
role="link"
aria-current="page"
aria-disabled="true"
className={cn("font-medium text-foreground", className)}
className={className}
{...props}
/>
)
@@ -67,30 +61,34 @@ function BreadcrumbPage({ className, ...props }: React.ComponentPropsWithoutRef<
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
return (
<li
<Box
data-slot="breadcrumb-separator"
component="li"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
className={className}
style={{ display: "flex", alignItems: "center" }}
{...props}
>
{children ?? <ChevronRightIcon />}
</li>
{children ?? <IconChevronRight size={14} />}
</Box>
)
}
function BreadcrumbEllipsis({ className, ...props }: React.ComponentPropsWithoutRef<"span">) {
return (
<span
<Box
data-slot="breadcrumb-ellipsis"
component="span"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
style={{ display: "flex", width: 36, height: 36, alignItems: "center", justifyContent: "center" }}
className={className}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More</span>
</span>
<IconDots size={16} />
<span style={{ position: "absolute", width: 1, height: 1, padding: 0, margin: -1, overflow: "hidden", clip: "rect(0,0,0,0)", whiteSpace: "nowrap", borderWidth: 0 }}>More</span>
</Box>
)
}
+5 -5
View File
@@ -6,14 +6,14 @@ domain: ui
version: "1.0.0"
purity: impure
signature: "Button(props: ButtonProps & VariantProps<typeof buttonVariants>): JSX.Element"
description: "Botón accesible con 6 variantes (default, outline, secondary, ghost, destructive, link) y 8 tamaños. Base-UI primitivo con CVA."
tags: [button, component, ui, interactive, cva]
uses_functions: [cn_ts_core]
description: "Botón accesible con 6 variantes (default, outline, secondary, ghost, destructive, link) y 8 tamaños. Mantine Button."
tags: [button, component, ui, interactive, mantine]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@base-ui/react", "class-variance-authority"]
imports: ["@mantine/core"]
output: "JSX.Element: botón renderizado con los estilos y comportamientos configurados"
tested: false
tests: []
@@ -51,4 +51,4 @@ source_file: "frontend/src/components/ui/button.tsx"
## Notas
Componente base del sistema. Usa Base-UI Button primitive para accesibilidad completa (keyboard, ARIA). CVA para gestión type-safe de variantes.
Componente base del sistema. Usa Mantine Button para accesibilidad completa (keyboard, ARIA). Las variantes se mapean a Mantine: default->filled, outline->outline, secondary->light, ghost->subtle, destructive->filled(red), link->transparent.
+51 -39
View File
@@ -1,52 +1,64 @@
"use client"
import * as React from 'react'
import { Button as MantineButton } from '@mantine/core'
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "../core/cn"
type ButtonVariant = 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link'
type ButtonSize = 'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'
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",
},
}
)
const variantMap: Record<ButtonVariant, { variant: string; color?: string }> = {
default: { variant: 'filled' },
outline: { variant: 'outline' },
secondary: { variant: 'light' },
ghost: { variant: 'subtle' },
destructive: { variant: 'filled', color: 'red' },
link: { variant: 'transparent' },
}
const sizeMap: Record<ButtonSize, { size: string; style?: React.CSSProperties }> = {
default: { size: 'sm' },
xs: { size: 'xs' },
sm: { size: 'xs' },
lg: { size: 'md' },
icon: { size: 'sm', style: { width: 32, height: 32, padding: 0 } },
'icon-xs': { size: 'xs', style: { width: 24, height: 24, padding: 0 } },
'icon-sm': { size: 'xs', style: { width: 28, height: 28, padding: 0 } },
'icon-lg': { size: 'md', style: { width: 36, height: 36, padding: 0 } },
}
/** Kept for backwards compatibility — maps variant names to Mantine equivalents */
const buttonVariants = variantMap
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
variant?: ButtonVariant
size?: ButtonSize
children?: React.ReactNode
}
function Button({
className,
variant = "default",
size = "default",
variant = 'default',
size = 'default',
style,
children,
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
}: ButtonProps) {
const mv = variantMap[variant]
const ms = sizeMap[size]
return (
<ButtonPrimitive
<MantineButton
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
variant={mv.variant}
color={mv.color}
size={ms.size}
radius="md"
className={className}
style={{ ...ms.style, ...style }}
{...props}
/>
>
{children}
</MantineButton>
)
}
export { Button, buttonVariants }
export type { ButtonProps, ButtonVariant, ButtonSize }
+1 -1
View File
@@ -8,7 +8,7 @@ purity: impure
signature: "Card(props: { size?: 'default' | 'sm'; variant?: 'default' | 'borderless' | 'ghost'; className?: string; children: ReactNode }): JSX.Element"
description: "Contenedor card con header, title, description, action, content y footer. Sistema de slots composable. Variantes default, borderless y ghost para dashboards dark."
tags: [card, container, layout, component, ui, dashboard, dark]
uses_functions: [cn_ts_core]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
+46 -36
View File
@@ -1,89 +1,99 @@
import * as React from "react"
import { cn } from "../core/cn"
import * as React from 'react'
import { Paper, Box, Text } from '@mantine/core'
type CardVariant = "default" | "borderless" | "ghost"
type CardVariant = 'default' | 'borderless' | 'ghost'
function Card({
className,
size = "default",
variant = "default",
size = 'default',
variant = 'default',
children,
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm"; variant?: CardVariant }) {
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm'; variant?: CardVariant }) {
return (
<div
<Paper
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
)}
withBorder={variant === 'default'}
shadow={variant === 'default' ? 'xs' : undefined}
radius="md"
p={size === 'sm' ? 'sm' : 'md'}
bg={variant === 'ghost' ? 'transparent' : undefined}
className={className}
{...props}
/>
>
{children}
</Paper>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
<Box
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
)}
pb="xs"
className={className}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
<Text
component="div"
data-slot="card-title"
className={cn("text-base leading-snug font-medium group-data-[size=sm]/card:text-sm", className)}
fw={600}
size="sm"
className={className}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
<Text
component="div"
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
size="sm"
c="dimmed"
className={className}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
function CardAction({ className, style, ...props }: React.ComponentProps<'div'>) {
return (
<div
<Box
data-slot="card-action"
className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
style={{ position: 'absolute', top: 0, right: 0, ...style }}
className={className}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
<Box
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
className={className}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
<Box
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)}
pt="sm"
mt="auto"
style={{ borderTop: '1px solid var(--mantine-color-default-border)' }}
className={className}
{...props}
/>
)
+9 -7
View File
@@ -6,15 +6,15 @@ domain: ui
version: "1.0.0"
purity: impure
signature: "ChartContainer(props: { children: ReactNode; height?: number | string }): JSX.Element"
description: "Base para todos los charts Recharts: container responsive, tooltip temático, legend y utilidades de colores por serie."
tags: [chart, container, recharts, base, visualization, component, ui]
uses_functions: [cn_ts_core, get_series_color_ts_core]
description: "Thin wrapper Paper y utilidades de colores/series para los charts @mantine/charts."
tags: [chart, container, mantine, base, visualization, component, ui]
uses_functions: []
uses_types: [ChartSeries_ts_ui]
returns: []
returns_optional: false
error_type: ""
imports: [recharts, react]
output: "Componente ChartContainer que renderiza base responsive para gráficos Recharts con tooltip y legend temáticos"
imports: ["@mantine/core"]
output: "Componente ChartContainer Paper wrapper y utilidades getSeriesColor/Series para charts Mantine"
tested: false
tests: []
test_file_path: ""
@@ -40,11 +40,13 @@ source_file: "frontend/src/components/ui/charts/chart-base.tsx"
## Ejemplo
```tsx
import { ChartContainer, getSeriesColor, type Series } from './chart_container'
<ChartContainer height={400}>
<RechartsLineChart data={data}>...</RechartsLineChart>
<MantineLineChart ... />
</ChartContainer>
```
## Notas
Exporta: ChartContainer, ChartTooltipContent, ChartTooltip, ChartLegend, chartColors, defaultColors, getSeriesColor, Series.
Exporta: ChartContainer, defaultColors, getSeriesColor, Series. Wrapper fino sobre Mantine Paper para layout uniforme de charts.
+12 -59
View File
@@ -1,14 +1,4 @@
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%))',
]
import { Paper } from '@mantine/core'
export const defaultColors = ['#3b82f6', '#22c55e', '#f59e0b', '#8b5cf6', '#ec4899']
@@ -19,62 +9,25 @@ export interface Series {
}
export function getSeriesColor(index: number, color?: string): string {
return color || defaultColors[index % defaultColors.length]
return color || defaultColors[index % defaultColors.length]!
}
interface ChartContainerProps {
children: React.ReactNode
className?: string
height?: number | string
}
export function ChartContainer({ children, className, height = 300 }: ChartContainerProps) {
export function ChartContainer({ children, 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>
<Paper p="md" style={{ height, minWidth: 100, minHeight: typeof height === 'number' ? Math.min(height, 100) : 100 }}>
{children}
</Paper>
)
}
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} />
}
/** @deprecated Mantine charts handle tooltips internally. Kept for index.ts compat. */
export function ChartTooltipContent() { return null }
/** @deprecated Mantine charts handle tooltips internally. Kept for index.ts compat. */
export function ChartTooltip() { return null }
/** @deprecated Mantine charts handle legends internally. Kept for index.ts compat. */
export function ChartLegend() { return null }

Some files were not shown because too many files have changed in this diff Show More