# fn_registry C++ — Design System

> **Fuente de verdad de la identidad visual para todas las apps C++ del registry.**
> Espejo directo de `frontend/DESIGN_SYSTEM.md` en mundo ImGui / OpenGL.

---

## 1. Principio

Todas las apps C++ del registry comparten **la misma identidad visual que las apps web**: Mantine v9 dark + indigo primary + Geist typography + `radius.md = 8` por defecto. No existen apps C++ con tema propio — si una app quiere diferenciarse, lo hace eligiendo colores semanticos (success, warning, primary...), nunca redefiniendo la paleta base.

El equivalente C++ de `FnMantineProvider` + `createTheme(...)` es **una sola llamada**:

```cpp
fn::run_app(config, render);   // AppConfig::theme = ThemeMode::FnDark por defecto
```

Dentro, `app_base` invoca `fn_tokens::apply_dark_theme()` tras crear el contexto ImGui. Equivale exactamente a:

```tsx
<FnMantineProvider theme={createTheme({
  primaryColor: 'indigo',
  primaryShade: { light: 6, dark: 4 },
  defaultRadius: 'md',
})} defaultColorScheme="dark">
```

---

## 2. Stack

| Capa | Libreria | Notas |
|------|----------|-------|
| UI base | **Dear ImGui** (vendored) | immediate mode, docking branch |
| Charts | **ImPlot** (vendored) | line/bar/pie/histogram/heatmap/sparkline |
| Node editor | `imgui-node-editor` (vendored) | solo para `shader_canvas` / DAGs |
| Gráficos gfx | OpenGL 3.3 core via GLFW | `gl_shader`, `gl_framebuffer`, `fullscreen_quad` |
| Profiling | Tracy (opt-in con `TRACY_ENABLE=ON`) | `tracy_zone_cpp_core` |
| Tokens | `cpp/functions/core/tokens.{h,cpp}` | equivalente a `@fn_library` |
| Componentes | `cpp/functions/core/*`, `viz/*`, `gfx/*` | wrappers que leen `fn_tokens` |
| Framework | `cpp/framework/app_base.{h,cpp}` | bootstrap unico de GLFW + ImGui + tema |
| Build | CMake 3.16+ | un `add_subdirectory` por app |

### Deny-list (no usar en apps C++)

- No `ImGui::StyleColorsDark/Light/Classic()` en el main de una app — el tema lo fija `app_base`
- No `ImVec4(...)` hardcoded en widgets — usar `fn_tokens::colors::*`
- No pixeles magicos para spacing/radius — usar `fn_tokens::spacing::*` / `radius::*`
- No ICU / gettext — textos directos, UTF-8 en el fuente
- No libs de UI ajenas (Qt, Nuklear, Gtk, etc.)
- No CSS, QSS ni recursos externos de estilo

---

## 3. `AppConfig` y modos de tema

```cpp
#include "app_base.h"

int main() {
    fn::AppConfig cfg;
    cfg.title = "my_app";
    cfg.width = 1400;
    cfg.height = 900;
    cfg.viewports = true;            // multi-viewport OK, el tema se mantiene
    // cfg.theme = fn::ThemeMode::FnDark; // ya es el default
    return fn::run_app(cfg, render);
}
```

| `ThemeMode` | Uso |
|---|---|
| `FnDark` (**default**) | Identidad del registry — Mantine v9 dark + indigo |
| `ImGuiDark` | Dark de stock (gris, rounding 0). Solo si estas depurando estilos |
| `ImGuiLight` | Light de stock. Para capturas o documentacion |
| `None` | No toca `ImGuiStyle`. La app se encarga (evitar salvo caso excepcional) |

**Regla:** nunca cambies `ThemeMode` salvo que tengas justificacion; si tu app necesita variaciones, expon-las como props semanticos (`kind: "danger"`, `variant: "subtle"`) en tus widgets, no como tema alternativo.

---

## 4. Tokens — `fn_tokens`

Todos en `cpp/functions/core/tokens.h`. Namespaces:

### 4.1 Colors — Mantine v9 dark + indigo

| Token | Mantine | Hex | Uso |
|---|---|---|---|
| `colors::primary` | indigo.6 | `#4C6EF5` | Botones, tabs activos, selecciones |
| `colors::primary_hover` | indigo.5 | `#5C7CFA` | Hover de primary |
| `colors::primary_light` | indigo.4 | `#748FFC` | Nav highlight, checkmarks, separator activo |
| `colors::primary_active` | indigo.7 | `#4263EB` | Pulsado |
| `colors::success` | green.6 | `#40C057` | Badges / toasts de exito |
| `colors::warning` | yellow.6 | `#FAB005` | Avisos |
| `colors::error` | red.6 | `#FA5252` | Errores, destructive |
| `colors::info` | blue.6 | `#228BE6` | Info, enlaces |
| `colors::bg` | dark.7 | `#1A1B1E` | Window bg (= Mantine body) |
| `colors::surface` | dark.6 | `#25262B` | Paper, Card, FrameBg |
| `colors::surface_hover` | dark.5 | `#2C2E33` | Hover de surface |
| `colors::surface_active` | dark.4 | `#373A40` | Pulsado de surface |
| `colors::border` | dark.4 | `#373A40` | Borders estandar |
| `colors::border_strong` | dark.3 | `#5C5F66` | Scrollbar grab active |
| `colors::text` | dark.0 | `#C1C2C5` | Texto primario |
| `colors::text_muted` | dark.2 | `#909296` | Subtitulos, axis labels |
| `colors::text_dim` | dark.3 | `#5C5F66` | Disabled |

### 4.2 Radius — `defaultRadius: 'md'`

| Token | Valor | Equivalente Mantine |
|---|---|---|
| `radius::none` | 0 | — |
| `radius::xs` | 2 | `--mantine-radius-xs` |
| `radius::sm` | 4 | `--mantine-radius-sm` |
| `radius::md` | **8** | `--mantine-radius-md` (**default**) |
| `radius::lg` | 12 | `--mantine-radius-lg` (adaptado, Mantine usa 16) |
| `radius::xl` | 16 | `--mantine-radius-xl` (adaptado, Mantine usa 32) |

ImGui aplica `md` en `WindowRounding`, `ChildRounding`, `PopupRounding`, `FrameRounding`, `ScrollbarRounding`. `sm` en `GrabRounding`, `TabRounding`.

### 4.3 Spacing — densificado para ImGui

Mantine usa 10/12/16/20/32 px para CSS; ImGui vive en densidad mayor (TUI-ish), asi que el mapeo se densifica:

| Token | ImGui | Mantine CSS |
|---|---|---|
| `spacing::xs` | 4 | 10 |
| `spacing::sm` | 8 | 12 |
| `spacing::md` | 12 | 16 |
| `spacing::lg` | 16 | 20 |
| `spacing::xl` | 24 | 32 |

Se usan en `WindowPadding`, `FramePadding`, `ItemSpacing`, `CellPadding`, `IndentSpacing`.

### 4.4 Font size

`font_size::xs|sm|md|lg|xl|xxl` = `10|12|14|18|24|32`. El default (14) coincide con `--mantine-font-size-sm` (tiempos mas compactos que el body web de 16).

---

## 5. Qué aplica `apply_dark_theme()`

Se invoca una vez desde `app_base` (o manualmente si usas `ThemeMode::None`). Configura:

- **Todos los `ImGuiCol_*`**: WindowBg, ChildBg, PopupBg, FrameBg (+Hovered/Active), Button (+Hovered/Active), Header (+Hovered/Active), Tab (+Hovered/Active/Unfocused), Separator (+Hovered/Active), Border, Text (+Disabled/Selected), CheckMark, SliderGrab (+Active), ResizeGrip (+Hovered/Active), Scrollbar (+Grab +GrabHovered +GrabActive), Table (Header +BorderLight +BorderStrong +RowBg +RowBgAlt), Docking (Preview +EmptyBg), PlotLines/PlotHistogram, DragDropTarget, Nav (Highlight +WindowingHighlight +WindowingDimBg), ModalWindowDimBg, TextSelectedBg, MenuBarBg, TitleBg (+Active +Collapsed)
- **Rounding**: Window, Child, Popup, Frame, Grab, Scrollbar, Tab
- **Padding/spacing**: WindowPadding, FramePadding, CellPadding, ItemSpacing, ItemInnerSpacing, IndentSpacing, ScrollbarSize, GrabMinSize
- **Borders**: WindowBorderSize/ChildBorderSize/PopupBorderSize = 1, FrameBorderSize/TabBorderSize = 0 (Mantine no pinta border en frames)
- **ImPlot** si esta linkado: FrameBg, PlotBg, PlotBorder, LegendBg/Border/Text, TitleText, InlayText, AxisText/Grid/Tick/Bg, Selection, Crosshairs + paddings

### Qué NO toca (capacidades ImGui intactas)

- Ni `io.ConfigFlags` (docking, viewports, keyboard nav, multi-viewport): los gestiona `AppConfig`
- Ni backends (GLFW/OpenGL init): los gestiona `app_base`
- Ni `ImGui::CreateContext` / `DestroyContext`
- Ni fuentes (si quieres Geist o una font custom, llama `io.Fonts->AddFontFromFileTTF(...)` antes de `apply_dark_theme`; el tema no pisa `io.Fonts`)
- Ni `ImPlot` colormaps/markers (solo colores de chrome)

---

## 6. Equivalencias `@fn_library` ↔ C++

| Web (`@fn_library`) | C++ (`cpp/functions/...`) | Notas |
|---|---|---|
| `FnMantineProvider` + `createTheme` | `fn::run_app` con `ThemeMode::FnDark` | Punto de entrada unico |
| `Button` | `core/button.h` — `fn::core::button(label, kind, size)` | `kind: Primary\|Secondary\|Subtle\|Danger`, `size: Sm\|Md\|Lg` |
| `FnActionIcon` | `core/icon_button.h` | 28x28 + tooltip |
| `Badge` | `core/badge.h` | `Default\|Success\|Warning\|Error\|Info\|Outline` |
| `Input` / `TextInput` | `core/text_input.h` | label muted arriba |
| `Select` | `core/select.h` | |
| `Dialog` | `core/modal_dialog.h` | patron begin/end |
| `Toast` / `notifications.show` | `core/toast.h` | stack bottom-right con fade-out |
| `PageHeader` | `core/page_header.h` | begin/end para meter acciones |
| `EmptyState` | `core/empty_state.h` | icono grande + titulo + desc |
| `Sheet` / `Drawer` | `core/sidebar.h` | panel lateral colapsable |
| `Tabs` | `core/tab_container.h` | |
| `AppShell` | `core/docking_layout.h` | preset de docking space |
| `Card` / `Paper` | `core/dashboard_panel.h` | surface + border + radius md |
| `SimpleGrid` | `core/dashboard_grid.h` | N columnas uniformes |
| `FnTree` / nav anidada | `core/tree_view.h` | |
| `KPICard` | `viz/kpi_card.h` | valor + delta + sparkline |
| `Sparkline` | `viz/sparkline.h` | mini chart inline |
| `LineChart` | `viz/line_plot.h` | |
| `BarChart` | `viz/bar_chart.h` | |
| `AreaChart` | `viz/line_plot.h` + flags ImPlot | |
| `PieChart` | `viz/pie_chart.h` | pie/donut |
| `DataTable` | `viz/table_view.h` | sorting + scroll |
| `GraphContainer` | `viz/graph_viewport.h` + `viz/graph_renderer.h` | sigma → ImGui GPU |
| `Tabler icons` | Glyphs UTF-8 en `icon_button` | evitar bundles de icon atlas en apps pequeñas |

---

## 7. Plantilla `main.cpp` para una app nueva

```cpp
#include "app_base.h"
#include "imgui.h"
#include "core/tokens.h"
#include "core/fullscreen_window.h"
#include "core/page_header.h"
#include "core/dashboard_grid.h"
#include "viz/kpi_card.h"

static void render() {
    fullscreen_window_begin("##my_app");

    page_header_begin("My App", "subtitle opcional");
    page_header_end();

    ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));

    dashboard_grid_begin(4);
      kpi_card("Total", "1,284", /* delta */ 12.0f, /* positive */ true);
      kpi_card("OK",    "97.3%",  0.4f, true);
      kpi_card("P95",   "842ms", -8.0f, true);
      kpi_card("Err",   "34",     5.0f, false);
    dashboard_grid_end();

    fullscreen_window_end();
}

int main() {
    fn::AppConfig cfg;
    cfg.title  = "my_app";
    cfg.width  = 1400;
    cfg.height = 900;
    return fn::run_app(cfg, render);
}
```

CMakeLists minimo de la app:

```cmake
add_imgui_app(my_app
    main.cpp
    ${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
    ${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
    ${CMAKE_SOURCE_DIR}/functions/core/dashboard_grid.cpp
    ${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
)
```

`fn_framework` ya incluye `tokens.cpp`, no hace falta añadirlo.

---

## 8. Anti-patrones (rechazar en review)

```cpp
// ❌ Hardcode de paleta
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.4f, 0.9f, 1.0f));
// ✅
ImGui::PushStyleColor(ImGuiCol_Button, fn_tokens::colors::primary);

// ❌ StyleColorsDark en la app — sobreescribe la identidad
ImGui::StyleColorsDark();

// ❌ apply_dark_theme en cada frame (no es caro pero es ruido)
if (!theme_applied) { fn_tokens::apply_dark_theme(); ... }
// ✅ run_app lo hace una vez al inicio

// ❌ Spacing magico
ImGui::Dummy(ImVec2(0, 13));
// ✅
ImGui::Dummy(ImVec2(0, fn_tokens::spacing::md));

// ❌ Mezclar Qt, Nuklear u otra lib de UI en apps/
// ✅ ImGui + ImPlot + los wrappers de cpp/functions/

// ❌ Meter un sub-tema para "destacar" una pantalla
cfg.theme = fn::ThemeMode::ImGuiLight; // romperia consistencia
// ✅ usar tokens semanticos (success/warning/...)
```

---

## 9. Checklist para una app C++ nueva

- [ ] `main.cpp` llama a `fn::run_app(...)` con `ThemeMode::FnDark` (default) — **no** `ImGui::StyleColorsDark`
- [ ] Cero `ImVec4(0.x, 0.y, ..., 1.0f)` — solo `fn_tokens::colors::*`
- [ ] Cero floats magicos en padding/spacing/rounding — solo `fn_tokens::spacing::*` / `radius::*`
- [ ] Componentes comunes desde `cpp/functions/core/`, `viz/`, `gfx/`
- [ ] Si creas un widget reutilizable, extraelo a `cpp/functions/<domain>/{name}.{h,cpp,md}` y `./fn index`
- [ ] `app.md` con frontmatter (`name`, `description`, `tags`, `lang: cpp`)
- [ ] CMakeLists usa `add_imgui_app(...)` — no `add_executable` directo
- [ ] Multi-viewport (`cfg.viewports = true`) si la app espera ser undock-able; el tema se adapta solo
- [ ] Tracy opt-in solo si interesa profiling (`-DTRACY_ENABLE=ON`)

---

## 10. Cambios respecto al estado previo (2026-04)

- Tokens alineados a valores **exactos** de Mantine v9 dark + indigo (antes aproximaban a ojo)
- `radius::md` ahora es 8 (antes 5) para casar con `defaultRadius: 'md'`
- `app_base::AppConfig` añade campo `theme` (default `FnDark`) y aplica el tema en `run_app`
- `fn_framework` incluye `tokens.cpp` — cualquier app que enlace el framework obtiene la identidad por default
- `fn_tokens::apply_dark_theme()` ahora estiliza tambien ImPlot si esta enlazado (colores de plot bg, axis, legend, crosshairs) y añade entradas que faltaban (docking preview, nav highlight, modal dim, scrollbars)
- `registry_dashboard` y `shaders_lab` ahora comparten look 100% sin cambios en sus propios mains

---

## 11. Iconos — Tabler (auto-cargado)

Espejo del frontend web (`@tabler/icons-react`). Set unico para todas las apps C++ del registry. **Sin glyph atlas custom por app, sin emojis Unicode arbitrarios.**

### Regla

Cualquier glyph en boton, menu, toolbar, tree o status line **DEBE** salir de los macros `TI_*` de `cpp/functions/core/icons_tabler.h`. No se permite:

- `"\xe2\x9a\x99"` o cualquier UTF-8 hex inline → usar `TI_SETTINGS`.
- Emojis Unicode (✓, ⚠, ❌, ⚙) → muchos no estan en el atlas y salen como `?` o cuadritos.
- `"?"` / `"!"` ASCII como sustituto → usar `TI_HELP`, `TI_ALERT_CIRCLE`.

### Como funciona

`fn::run_app` (en `framework/app_base.cpp`) llama a `fn_ui::load_default_fonts()` justo despues de crear el contexto ImGui. Carga **Karla-Regular** (texto vectorial, 13 px, vendoreado por ImGui en `vendor/imgui/misc/fonts/`) + mergea `tabler-icons.ttf` (5093 glyphs, U+E000..U+FCFF) al mismo tamaño en el mismo `ImFont`. Texto e iconos comparten line-height. A partir de ahi:

```cpp
#include "core/icons_tabler.h"

button(TI_PLUS " New",         V::Primary);
button(TI_DEVICE_FLOPPY " Save", V::Secondary);
icon_button("##del", TI_TRASH,    "Delete");
icon_button("##cfg", TI_SETTINGS, "Settings");
```

Texto y glyph se mezclan en la misma string porque comparten atlas.

### Catalogo

`cpp/functions/core/icons_tabler.h` (generado por `cpp/vendor/tabler-icons/gen_header.py` desde el CSS oficial v3.41.1). Buscar un icono:

```bash
grep -i "TI_TRASH\|TI_DELETE" cpp/functions/core/icons_tabler.h
# o explorar en https://tabler.io/icons (mismo set)
```

Nombres: `TI_<NAME_UPPER_SNAKE>` derivado del nombre kebab-case del icono Tabler (`tabler-trash` → `TI_TRASH`, `tabler-device-floppy` → `TI_DEVICE_FLOPPY`).

### Path resolution en runtime

`icon_font.cpp` busca `Karla-Regular.ttf` y `tabler-icons.ttf` con el mismo orden (primer match gana):

1. `./<filename>` (cwd / junto al exe — `add_imgui_app` copia ambas post-build)
2. `./assets/<filename>`
3. `$FN_ASSETS_DIR/<filename>`
4. `${FN_CPP_ROOT}/<repo_subpath>` (compile-time define):
   - Karla → `vendor/imgui/misc/fonts/Karla-Regular.ttf`
   - Tabler → `vendor/tabler-icons/tabler-icons.ttf`

Fallbacks: si falta Karla, ImGui usa ProggyClean (bitmap, default historico). Si falta Tabler, los `TI_*` salen como cuadritos. Indicadores programaticos: `fn_ui::text_font_loaded()`, `fn_ui::tabler_font_loaded()`.

### Upgrade de version

```bash
cd cpp/vendor/tabler-icons
curl -sL -o tabler-icons.ttf "https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@<NEW_VERSION>/dist/fonts/tabler-icons.ttf"
curl -sL -o tabler-icons.css "https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@<NEW_VERSION>/dist/tabler-icons.css"
python3 gen_header.py    # regenera ../../functions/core/icons_tabler.h
```

Bump tambien `TI_FONT_VERSION` queda actualizado automaticamente.

### Anti-patrones (rechazar en review)

```cpp
// ❌ Hex UTF-8 inline — pre-Tabler, no renderiza en el atlas actual
icon_button("##rl", "\xe2\x86\xbb", "Reload");
// ✅
icon_button("##rl", TI_REFRESH, "Reload");

// ❌ Emoji
button("✓ OK", V::Primary);
// ✅
button(TI_CHECK " OK", V::Primary);

// ❌ Cargar otra fuente de iconos en el app (FontAwesome, Material...)
io.Fonts->AddFontFromFileTTF("fa-solid.ttf", ...);
// ✅ usar TI_* — ya esta cargado por fn::run_app

// ❌ Bypass del path resolver hardcodeando una ruta absoluta
io.Fonts->AddFontFromFileTTF("/home/lucas/.../tabler.ttf", ...);
// ✅ confiar en load_default_fonts() / FN_ASSETS_DIR
```

### Checklist (añadir a la del punto 9)

- [ ] Cero `"\x..\x.."` en codigo de UI — solo `TI_*` de `core/icons_tabler.h`
- [ ] Cero emojis Unicode en strings de boton / menu / toolbar
- [ ] Si el icono que necesito no existe en Tabler, replantear el flujo (en general hay equivalente)
- [ ] No cargar fuentes adicionales sin justificacion (el atlas merge es delicado)

---

## 12. Settings — ventana flotante + persistencia

Toda app C++ del registry hereda de `fn::run_app` un menu **Settings...** en la `MainMenuBar` (junto a `View` / `Layouts`) que abre una ventana flotante con:

- **Display** → toggle `Show FPS overlay` (overlay top-right con fps + ms)
- **Typography** → combo de fuente (Karla / Roboto / DroidSans / Cousine / ProggyClean) + combo de tamaño (12/13/14/15/16/18/20 px) + slider libre 10..32 px
- **Secciones extra registradas por la app** (si las hay)

### Persistencia — `app_settings.ini`

Junto al ejecutable. Auto-save al cambiar (sin botones). Cada app tiene su propio `.ini` porque cada exe corre desde su carpeta:

```ini
# fn_registry app_settings.ini — autogenerado, editable
show_fps     = 1
font_id      = 0   # 0=Karla 1=Roboto 2=DroidSans 3=Cousine 4=ProggyClean
font_size_px = 15.0
```

### Extender desde una app

```cpp
#include "core/app_settings.h"

int main() {
    fn_ui::settings_window_add_section("shader_compiler", "Shader compiler", []{
        ImGui::Checkbox("Auto-compile on save", &g_auto_compile);
        ImGui::SliderInt("Debounce (ms)", &g_debounce_ms, 50, 2000);
    });
    return fn::run_app({...}, render);
}
```

La seccion aparece debajo de las core, dentro de un `CollapsingHeader` abierto por defecto. La app es responsable de persistir su estado (su SQLite, env, archivo aparte) — el `.ini` core solo guarda `show_fps`/`font_id`/`font_size_px`.

### Cambio de fuente en runtime

`run_app` consume `settings_consume_font_dirty()` al inicio de cada frame: si hay cambio, ejecuta `io.Fonts->Clear()` + `load_fonts_from_settings()`. ImGui 1.92+ refresca la GPU texture automaticamente via `ImGui_ImplOpenGL3_UpdateTexture` al siguiente `NewFrame`. Coste: un frame de hiccup, sin reinicio.

### Defaults

- `font_id = Karla` — humanist sans-serif vectorial, vendoreado por ImGui (cero descargas), nitida a 13..18 px.
- `font_size_px = 15` — un punto por encima de ImGui default (13) para legibilidad sin sacrificar densidad. Si una app necesita mas detalle, el usuario lo bumpea a 18/20.
- `show_fps = false` — overlay opcional, no contamina capturas por defecto.

### Anti-patron

```cpp
// ❌ Fps overlay hardcoded en el render — el toggle de Settings no lo apaga
fps_overlay();
// ✅ El overlay lo gestiona run_app via settings().show_fps. La app no llama nada.

// ❌ AddFontFromFileTTF directo en main — pisa load_fonts_from_settings
io.Fonts->AddFontFromFileTTF("MyFont.ttf", 14);
// ✅ Si necesitas una fuente fija no-configurable, hazlo en una nueva FontId
//    y un nuevo archivo .ttf vendoreado, no en codigo de app.
```
