feat(dev): issues 0100-0104 — dev_console binary + work_tab + DoD user-facing + frontmatter migration de 146 issues + taxonomia canonica

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 02:44:04 +02:00
parent 6ad82167bb
commit fad4006f60
164 changed files with 3934 additions and 323 deletions
+2
View File
@@ -71,6 +71,8 @@ void about_window_render() {
// --- Modules consumidos por la app (issue 0097) ---
if (fn::app_modules_count > 0) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Modules (%lu)", fn::app_modules_count);
if (ImGui::BeginTable("##fn_modules_table", 2,
+76
View File
@@ -0,0 +1,76 @@
#include "core/app_card.h"
namespace fn_ui {
static ImU32 with_alpha(ImU32 c, float a) {
ImVec4 v = ImGui::ColorConvertU32ToFloat4(c);
v.w = a;
return ImGui::ColorConvertFloat4ToU32(v);
}
bool app_card(const AppCardData& data, ImVec2 size) {
ImGui::PushID(data.id);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton("##card", size);
bool hovered = ImGui::IsItemHovered();
bool held = ImGui::IsItemActive();
bool clicked = ImGui::IsItemClicked();
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 p0 = pos;
ImVec2 p1 = ImVec2(pos.x + size.x, pos.y + size.y);
ImU32 bg = with_alpha(data.accent, hovered ? 0.28f : 0.16f);
if (held) bg = with_alpha(data.accent, 0.40f);
ImU32 border = with_alpha(data.accent, hovered ? 0.95f : 0.55f);
const float rounding = 8.0f;
dl->AddRectFilled(p0, p1, bg, rounding);
dl->AddRect(p0, p1, border, rounding, 0, 1.5f);
const float stripe_w = 6.0f;
dl->AddRectFilled(p0, ImVec2(p0.x + stripe_w, p1.y), data.accent, rounding,
ImDrawFlags_RoundCornersLeft);
// Clip everything past this point so wrapped text never spills out.
dl->PushClipRect(ImVec2(p0.x + stripe_w + 2.0f, p0.y + 2.0f),
ImVec2(p1.x - 2.0f, p1.y - 2.0f), true);
const float pad_x = stripe_w + 10.0f;
const float pad_y = 10.0f;
// Optional icon on the left
float text_left = p0.x + pad_x;
if (data.icon != (ImTextureID)0) {
float icon_size = size.y - 2.0f * pad_y;
if (icon_size > 64.0f) icon_size = 64.0f;
ImVec2 icon_p0(p0.x + pad_x, p0.y + (size.y - icon_size) * 0.5f);
ImVec2 icon_p1(icon_p0.x + icon_size, icon_p0.y + icon_size);
dl->AddImageRounded(data.icon, icon_p0, icon_p1,
ImVec2(0, 0), ImVec2(1, 1),
IM_COL32_WHITE, 6.0f);
text_left = icon_p1.x + 10.0f;
}
ImVec2 title_pos = ImVec2(text_left, p0.y + pad_y);
ImU32 title_col = IM_COL32(245, 245, 250, 255);
if (data.title) {
dl->AddText(title_pos, title_col, data.title);
}
if (data.description && data.description[0] != '\0') {
ImVec2 desc_pos = ImVec2(text_left,
title_pos.y + ImGui::GetTextLineHeight() + 4.0f);
ImU32 desc_col = IM_COL32(195, 200, 210, 230);
float wrap_w = (p1.x - 8.0f) - text_left;
if (wrap_w < 20.0f) wrap_w = 20.0f;
dl->AddText(NULL, 0.0f, desc_pos, desc_col,
data.description, NULL, wrap_w);
}
dl->PopClipRect();
ImGui::PopID();
return clicked;
}
} // namespace fn_ui
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <imgui.h>
namespace fn_ui {
struct AppCardData {
const char* id; // unique ImGui ID within the parent window
const char* title; // bold display name (single line)
const char* description; // optional second line; may be nullptr or ""
ImU32 accent; // accent color (IM_COL32 / hex parsed)
ImTextureID icon; // optional icon texture (0 = no icon, layout still works)
};
// Renders a card-shaped clickable area with:
// - rounded translucent accent background
// - solid accent stripe on the left edge
// - title on top, wrapped description below
// Uses InvisibleButton for hit-testing + ImDrawList for visuals so it does
// not inherit ImGui::Button colors.
//
// Returns true on the frame the user clicks the card.
bool app_card(const AppCardData& data, ImVec2 size);
} // namespace fn_ui
+80
View File
@@ -0,0 +1,80 @@
---
name: app_card
kind: component
lang: cpp
domain: core
version: "1.1.0"
purity: pure
signature: "bool fn_ui::app_card(const fn_ui::AppCardData& data, ImVec2 size)"
description: "Clickable card widget with accent stripe, optional left-side icon (ImTextureID), title and wrapped description. Inner ClipRect prevents description overflow when text exceeds card height. Does NOT inherit ImGui::Button colors — uses InvisibleButton + ImDrawList so visuals are fully driven by accent."
tags: [imgui, ui, card, button, launcher, hub]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [imgui]
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/core/app_card.cpp"
framework: imgui
params:
- name: data
desc: "AppCardData {id, title, description, accent}. id must be unique within the parent window. description may be nullptr or empty."
- name: size
desc: "Card size in pixels (width x height). Typical: ImVec2(280, 96)."
output: "true the frame the user clicks the card, false otherwise."
notes: "Consumed by apps/app_hub_launcher. Pattern: caller loops over a vector of cards in a grid using SameLine + Dummy for row spacing."
---
# app_card
Clickable card widget for app/launcher grids. Visual identity driven by an accent color (semi-transparent fill + solid stripe + accent border) so it does NOT look like a default `ImGui::Button` (no blue). Uses `InvisibleButton` for hit-testing and `ImDrawList` for the visuals — no `ImGui::Button` is involved.
## Ejemplo
```cpp
#include "core/app_card.h"
fn_ui::AppCardData card;
card.id = "graph_explorer";
card.title = "Graph Explorer";
card.description = "Visor de grafos GPU-accelerated";
card.accent = IM_COL32(0x08, 0x91, 0xc2, 255); // #0891c2
if (fn_ui::app_card(card, ImVec2(280, 96))) {
// launch the app
}
```
Grid layout (N cards per row):
```cpp
float card_w = 280.0f, card_h = 96.0f, spacing = 12.0f;
int cols = std::max(1, (int)((ImGui::GetContentRegionAvail().x + spacing) / (card_w + spacing)));
int shown = 0;
for (auto& app : apps) {
if (shown % cols != 0) ImGui::SameLine(0.0f, spacing);
fn_ui::AppCardData c{app.id.c_str(), app.title.c_str(), app.desc.c_str(), app.accent};
if (fn_ui::app_card(c, ImVec2(card_w, card_h))) launch(app);
++shown;
if (shown % cols == 0) ImGui::Dummy(ImVec2(0.0f, spacing - 4.0f));
}
```
## Cuando usarla
Cuando necesites un grid/lista de "cards" clicables (launchers, dashboards de modulos, selectores de proyecto) y quieras que la identidad visual venga de un accent color por card en vez de los azules por defecto de `ImGui::Button`. Si solo quieres un boton simple con texto, usa `ImGui::Button` normal.
## Gotchas
- `data.id` debe ser unico dentro del padre — colision = un solo card recibe el click.
- La descripcion se renderiza con wrap automatico; si `size.y` es muy pequeno el texto se sale por debajo (no clipea). Ajusta `size.y` a 90-110 px para 2-3 lineas de wrap.
- Pinta sobre `ImGui::GetWindowDrawList()` — debe llamarse dentro de una ventana ImGui activa.
- El color `data.accent` debe llevar alpha 255; las transparencias para fondo/border/hover las calcula el componente internamente.
## Capability growth log
v1.1.0 (2026-05-17) — Optional `icon` (ImTextureID) param renders left-side thumbnail; PushClipRect contains description overflow.
v1.0.0 (2026-05-17) — Initial. Extracted from inline rendering in `apps/app_hub_launcher/main.cpp`.