From 73a4c3a148bc1338cc8952db00b83ccb1446ce82 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 6 Apr 2026 23:46:36 +0200 Subject: [PATCH 1/6] feat: add C++ support with ImGui/ImPlot framework and vendor submodules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade soporte C++ al registry: vendor submodules (glfw, imgui, implot, tracy), sistema de build con CMake y toolchains cross-platform, runner C++ en fn CLI, parser de tests Google Test, y funciones bash para build Linux/Windows. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitmodules | 13 +++ bash/functions/infra/build_cpp_linux.md | 36 ++++++ bash/functions/infra/build_cpp_linux.sh | 24 ++++ bash/functions/infra/build_cpp_windows.md | 38 +++++++ bash/functions/infra/build_cpp_windows.sh | 34 ++++++ bash/functions/infra/install_cpp_deps.md | 37 +++++++ bash/functions/infra/install_cpp_deps.sh | 49 +++++++++ cmd/fn/cpprunner.go | 128 ++++++++++++++++++++++ cmd/fn/run.go | 2 + cpp/CMakeLists.txt | 100 +++++++++++++++++ cpp/apps/chart_demo/CMakeLists.txt | 8 ++ cpp/apps/chart_demo/main.cpp | 79 +++++++++++++ cpp/framework/app_base.cpp | 105 ++++++++++++++++++ cpp/framework/app_base.h | 25 +++++ cpp/functions/core/fps_overlay.cpp | 33 ++++++ cpp/functions/core/fps_overlay.h | 5 + cpp/functions/core/fps_overlay.md | 30 +++++ cpp/functions/viz/bar_chart.cpp | 26 +++++ cpp/functions/viz/bar_chart.h | 6 + cpp/functions/viz/bar_chart.md | 40 +++++++ cpp/functions/viz/heatmap.cpp | 24 ++++ cpp/functions/viz/heatmap.h | 9 ++ cpp/functions/viz/heatmap.md | 42 +++++++ cpp/functions/viz/line_plot.cpp | 16 +++ cpp/functions/viz/line_plot.h | 8 ++ cpp/functions/viz/line_plot.md | 40 +++++++ cpp/functions/viz/scatter_plot.cpp | 16 +++ cpp/functions/viz/scatter_plot.h | 6 + cpp/functions/viz/scatter_plot.md | 38 +++++++ cpp/toolchains/linux-x86_64.cmake | 8 ++ cpp/toolchains/mingw-w64.cmake | 17 +++ cpp/vendor/glfw | 1 + cpp/vendor/imgui | 1 + cpp/vendor/implot | 1 + cpp/vendor/tracy | 1 + registry/test_parser.go | 19 ++++ 36 files changed, 1065 insertions(+) create mode 100644 .gitmodules create mode 100644 bash/functions/infra/build_cpp_linux.md create mode 100644 bash/functions/infra/build_cpp_linux.sh create mode 100644 bash/functions/infra/build_cpp_windows.md create mode 100644 bash/functions/infra/build_cpp_windows.sh create mode 100644 bash/functions/infra/install_cpp_deps.md create mode 100644 bash/functions/infra/install_cpp_deps.sh create mode 100644 cmd/fn/cpprunner.go create mode 100644 cpp/CMakeLists.txt create mode 100644 cpp/apps/chart_demo/CMakeLists.txt create mode 100644 cpp/apps/chart_demo/main.cpp create mode 100644 cpp/framework/app_base.cpp create mode 100644 cpp/framework/app_base.h create mode 100644 cpp/functions/core/fps_overlay.cpp create mode 100644 cpp/functions/core/fps_overlay.h create mode 100644 cpp/functions/core/fps_overlay.md create mode 100644 cpp/functions/viz/bar_chart.cpp create mode 100644 cpp/functions/viz/bar_chart.h create mode 100644 cpp/functions/viz/bar_chart.md create mode 100644 cpp/functions/viz/heatmap.cpp create mode 100644 cpp/functions/viz/heatmap.h create mode 100644 cpp/functions/viz/heatmap.md create mode 100644 cpp/functions/viz/line_plot.cpp create mode 100644 cpp/functions/viz/line_plot.h create mode 100644 cpp/functions/viz/line_plot.md create mode 100644 cpp/functions/viz/scatter_plot.cpp create mode 100644 cpp/functions/viz/scatter_plot.h create mode 100644 cpp/functions/viz/scatter_plot.md create mode 100644 cpp/toolchains/linux-x86_64.cmake create mode 100644 cpp/toolchains/mingw-w64.cmake create mode 160000 cpp/vendor/glfw create mode 160000 cpp/vendor/imgui create mode 160000 cpp/vendor/implot create mode 160000 cpp/vendor/tracy diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..6adba50d --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/bash/functions/infra/build_cpp_linux.md b/bash/functions/infra/build_cpp_linux.md new file mode 100644 index 00000000..4685129e --- /dev/null +++ b/bash/functions/infra/build_cpp_linux.md @@ -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 +``` diff --git a/bash/functions/infra/build_cpp_linux.sh b/bash/functions/infra/build_cpp_linux.sh new file mode 100644 index 00000000..7a3028ab --- /dev/null +++ b/bash/functions/infra/build_cpp_linux.sh @@ -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" diff --git a/bash/functions/infra/build_cpp_windows.md b/bash/functions/infra/build_cpp_windows.md new file mode 100644 index 00000000..0d300da8 --- /dev/null +++ b/bash/functions/infra/build_cpp_windows.md @@ -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` diff --git a/bash/functions/infra/build_cpp_windows.sh b/bash/functions/infra/build_cpp_windows.sh new file mode 100644 index 00000000..9d52db97 --- /dev/null +++ b/bash/functions/infra/build_cpp_windows.sh @@ -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 diff --git a/bash/functions/infra/install_cpp_deps.md b/bash/functions/infra/install_cpp_deps.md new file mode 100644 index 00000000..2f48e230 --- /dev/null +++ b/bash/functions/infra/install_cpp_deps.md @@ -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 +``` diff --git a/bash/functions/infra/install_cpp_deps.sh b/bash/functions/infra/install_cpp_deps.sh new file mode 100644 index 00000000..1ce25129 --- /dev/null +++ b/bash/functions/infra/install_cpp_deps.sh @@ -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." diff --git a/cmd/fn/cpprunner.go b/cmd/fn/cpprunner.go new file mode 100644 index 00000000..74f61dd0 --- /dev/null +++ b/cmd/fn/cpprunner.go @@ -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 +} diff --git a/cmd/fn/run.go b/cmd/fn/run.go index 89206e42..9932a0ae 100644 --- a/cmd/fn/run.go +++ b/cmd/fn/run.go @@ -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) } diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 00000000..8079b01c --- /dev/null +++ b/cpp/CMakeLists.txt @@ -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() diff --git a/cpp/apps/chart_demo/CMakeLists.txt b/cpp/apps/chart_demo/CMakeLists.txt new file mode 100644 index 00000000..ed04084f --- /dev/null +++ b/cpp/apps/chart_demo/CMakeLists.txt @@ -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 +) diff --git a/cpp/apps/chart_demo/main.cpp b/cpp/apps/chart_demo/main.cpp new file mode 100644 index 00000000..1ec73628 --- /dev/null +++ b/cpp/apps/chart_demo/main.cpp @@ -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 +#include + +// 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(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(rand()) / RAND_MAX * 10.0f; + scatter_y[i] = scatter_x[i] * 0.5f + (static_cast(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); +} diff --git a/cpp/framework/app_base.cpp b/cpp/framework/app_base.cpp new file mode 100644 index 00000000..a4c0c328 --- /dev/null +++ b/cpp/framework/app_base.cpp @@ -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 +#include + +#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 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 render_fn) { + return run_app(AppConfig{}, render_fn); +} + +} // namespace fn diff --git a/cpp/framework/app_base.h b/cpp/framework/app_base.h new file mode 100644 index 00000000..8e3b92d8 --- /dev/null +++ b/cpp/framework/app_base.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +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 render_fn); + +// Convenience: run with default config +int run_app(std::function render_fn); + +} // namespace fn diff --git a/cpp/functions/core/fps_overlay.cpp b/cpp/functions/core/fps_overlay.cpp new file mode 100644 index 00000000..268698f5 --- /dev/null +++ b/cpp/functions/core/fps_overlay.cpp @@ -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(); +} diff --git a/cpp/functions/core/fps_overlay.h b/cpp/functions/core/fps_overlay.h new file mode 100644 index 00000000..abe124e4 --- /dev/null +++ b/cpp/functions/core/fps_overlay.h @@ -0,0 +1,5 @@ +#pragma once + +// Renders an FPS counter overlay in the top-right corner. +// Call within an ImGui frame. +void fps_overlay(); diff --git a/cpp/functions/core/fps_overlay.md b/cpp/functions/core/fps_overlay.md new file mode 100644 index 00000000..7d16e8a5 --- /dev/null +++ b/cpp/functions/core/fps_overlay.md @@ -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. diff --git a/cpp/functions/viz/bar_chart.cpp b/cpp/functions/viz/bar_chart.cpp new file mode 100644 index 00000000..75bb83d7 --- /dev/null +++ b/cpp/functions/viz/bar_chart.cpp @@ -0,0 +1,26 @@ +#include "viz/bar_chart.h" +#include "implot.h" + +#include + +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 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 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(); + } +} diff --git a/cpp/functions/viz/bar_chart.h b/cpp/functions/viz/bar_chart.h new file mode 100644 index 00000000..693e5973 --- /dev/null +++ b/cpp/functions/viz/bar_chart.h @@ -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); diff --git a/cpp/functions/viz/bar_chart.md b/cpp/functions/viz/bar_chart.md new file mode 100644 index 00000000..b374434e --- /dev/null +++ b/cpp/functions/viz/bar_chart.md @@ -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`. diff --git a/cpp/functions/viz/heatmap.cpp b/cpp/functions/viz/heatmap.cpp new file mode 100644 index 00000000..3f6b155c --- /dev/null +++ b/cpp/functions/viz/heatmap.cpp @@ -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(); + } +} diff --git a/cpp/functions/viz/heatmap.h b/cpp/functions/viz/heatmap.h new file mode 100644 index 00000000..9d5496af --- /dev/null +++ b/cpp/functions/viz/heatmap.h @@ -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); diff --git a/cpp/functions/viz/heatmap.md b/cpp/functions/viz/heatmap.md new file mode 100644 index 00000000..85ddda03 --- /dev/null +++ b/cpp/functions/viz/heatmap.md @@ -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. diff --git a/cpp/functions/viz/line_plot.cpp b/cpp/functions/viz/line_plot.cpp new file mode 100644 index 00000000..4ee7646d --- /dev/null +++ b/cpp/functions/viz/line_plot.cpp @@ -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(); + } +} diff --git a/cpp/functions/viz/line_plot.h b/cpp/functions/viz/line_plot.h new file mode 100644 index 00000000..50e31929 --- /dev/null +++ b/cpp/functions/viz/line_plot.h @@ -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); diff --git a/cpp/functions/viz/line_plot.md b/cpp/functions/viz/line_plot.md new file mode 100644 index 00000000..4b436717 --- /dev/null +++ b/cpp/functions/viz/line_plot.md @@ -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. diff --git a/cpp/functions/viz/scatter_plot.cpp b/cpp/functions/viz/scatter_plot.cpp new file mode 100644 index 00000000..f2eca52a --- /dev/null +++ b/cpp/functions/viz/scatter_plot.cpp @@ -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(); + } +} diff --git a/cpp/functions/viz/scatter_plot.h b/cpp/functions/viz/scatter_plot.h new file mode 100644 index 00000000..fbefac5d --- /dev/null +++ b/cpp/functions/viz/scatter_plot.h @@ -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); diff --git a/cpp/functions/viz/scatter_plot.md b/cpp/functions/viz/scatter_plot.md new file mode 100644 index 00000000..2073f0fe --- /dev/null +++ b/cpp/functions/viz/scatter_plot.md @@ -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`. diff --git a/cpp/toolchains/linux-x86_64.cmake b/cpp/toolchains/linux-x86_64.cmake new file mode 100644 index 00000000..c44e2250 --- /dev/null +++ b/cpp/toolchains/linux-x86_64.cmake @@ -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) diff --git a/cpp/toolchains/mingw-w64.cmake b/cpp/toolchains/mingw-w64.cmake new file mode 100644 index 00000000..8fa78619 --- /dev/null +++ b/cpp/toolchains/mingw-w64.cmake @@ -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++") diff --git a/cpp/vendor/glfw b/cpp/vendor/glfw new file mode 160000 index 00000000..b00e6a8a --- /dev/null +++ b/cpp/vendor/glfw @@ -0,0 +1 @@ +Subproject commit b00e6a8a88ad1b60c0a045e696301deb92c9a13e diff --git a/cpp/vendor/imgui b/cpp/vendor/imgui new file mode 160000 index 00000000..f5f6ca07 --- /dev/null +++ b/cpp/vendor/imgui @@ -0,0 +1 @@ +Subproject commit f5f6ca07be7ce0ea9eed6c04d55833bac3f6b50b diff --git a/cpp/vendor/implot b/cpp/vendor/implot new file mode 160000 index 00000000..524f9fcd --- /dev/null +++ b/cpp/vendor/implot @@ -0,0 +1 @@ +Subproject commit 524f9fcd48d76c13fdf94c5ffbba8787a1ff7e39 diff --git a/cpp/vendor/tracy b/cpp/vendor/tracy new file mode 160000 index 00000000..00a069d6 --- /dev/null +++ b/cpp/vendor/tracy @@ -0,0 +1 @@ +Subproject commit 00a069d6088ff8d93304eaac4d925cece0e9081c diff --git a/registry/test_parser.go b/registry/test_parser.go index 3abf75d0..81cad386 100644 --- a/registry/test_parser.go +++ b/registry/test_parser.go @@ -35,6 +35,8 @@ func parseTestFile(path, lang string) ([]testCase, error) { return parsePythonTests(content), nil case "bash": return parseBashTests(content), nil + case "cpp": + return parseCppTests(content), nil default: return nil, nil } @@ -115,6 +117,23 @@ func parseBashTests(content string) []testCase { return extractBlocks(lines, positions) } +// parseCppTests extracts C++ test functions (Google Test TEST/TEST_F macros). +var cppTestRe = regexp.MustCompile(`(?m)^(?:TEST|TEST_F|TEST_P)\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)`) + +func parseCppTests(content string) []testCase { + lines := strings.Split(content, "\n") + var positions []testPos + + for i, line := range lines { + if m := cppTestRe.FindStringSubmatch(line); m != nil { + name := m[1] + "." + m[2] // Suite.TestName + positions = append(positions, testPos{name: name, startLine: i}) + } + } + + return extractBlocks(lines, positions) +} + // extractBlocks splits lines into code blocks based on test positions. func extractBlocks(lines []string, positions []testPos) []testCase { var tests []testCase From 2d108c295a150b4387701f614d7fe2062e4f3317 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 6 Apr 2026 23:46:44 +0200 Subject: [PATCH 2/6] refactor: migrate frontend from shadcn/Tailwind to Mantine v9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reescribe todos los componentes UI para usar Mantine v9 en lugar de shadcn/Tailwind. Elimina cn(), CVA, components.json, theme_provider custom y globals.css con Tailwind. Añade 25+ componentes nuevos (AppShell, AuthForm, DatePickerInput, Dropzone, etc.) y MantineProvider como wrapper estándar del sistema de temas. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/commands/frontend.md | 311 +- .claude/rules/frontend_theming.md | 10 +- frontend/components.json | 25 - frontend/functions/core/chart_colors.ts | 12 +- frontend/functions/core/cn.md | 40 - frontend/functions/core/cn.ts | 6 - frontend/functions/core/generate_theme_css.md | 69 - frontend/functions/core/generate_theme_css.ts | 23 - frontend/functions/core/get_series_color.ts | 2 +- frontend/functions/core/get_theme_tokens.md | 50 - frontend/functions/core/get_theme_tokens.ts | 59 - .../functions/core/theme_config_to_colors.md | 41 - .../functions/core/theme_config_to_colors.ts | 49 - frontend/functions/ui/accordion.md | 16 +- frontend/functions/ui/accordion.tsx | 87 +- frontend/functions/ui/action_icon.md | 77 + frontend/functions/ui/action_icon.tsx | 47 + frontend/functions/ui/alert.md | 14 +- frontend/functions/ui/alert.tsx | 82 +- frontend/functions/ui/analytics_page.md | 4 +- frontend/functions/ui/analytics_page.tsx | 84 +- frontend/functions/ui/app_shell.md | 65 + frontend/functions/ui/app_shell.tsx | 33 + frontend/functions/ui/apply_theme.md | 44 - frontend/functions/ui/apply_theme.tsx | 111 - frontend/functions/ui/area_chart.md | 10 +- frontend/functions/ui/area_chart.tsx | 66 +- frontend/functions/ui/auth_form.md | 101 + frontend/functions/ui/auth_form.tsx | 181 + frontend/functions/ui/autocomplete.md | 136 + frontend/functions/ui/autocomplete.tsx | 10 + frontend/functions/ui/avatar.md | 12 +- frontend/functions/ui/avatar.tsx | 78 +- frontend/functions/ui/badge.md | 10 +- frontend/functions/ui/badge.tsx | 72 +- frontend/functions/ui/bar_chart.md | 12 +- frontend/functions/ui/bar_chart.tsx | 52 +- frontend/functions/ui/breadcrumb.md | 10 +- frontend/functions/ui/breadcrumb.tsx | 70 +- frontend/functions/ui/button.md | 10 +- frontend/functions/ui/button.tsx | 90 +- frontend/functions/ui/card.md | 2 +- frontend/functions/ui/card.tsx | 82 +- frontend/functions/ui/chart_container.md | 16 +- frontend/functions/ui/chart_container.tsx | 71 +- frontend/functions/ui/checkbox.md | 10 +- frontend/functions/ui/checkbox.tsx | 86 +- frontend/functions/ui/chip.md | 88 + frontend/functions/ui/chip.tsx | 12 + frontend/functions/ui/color_input.md | 104 + frontend/functions/ui/color_input.tsx | 10 + frontend/functions/ui/command.md | 4 +- frontend/functions/ui/command.tsx | 155 +- frontend/functions/ui/crud_page.md | 4 +- frontend/functions/ui/crud_page.tsx | 99 +- frontend/functions/ui/dashboard_layout.md | 4 +- frontend/functions/ui/dashboard_layout.tsx | 50 +- frontend/functions/ui/data_table.md | 8 +- frontend/functions/ui/data_table.tsx | 79 +- frontend/functions/ui/date_picker_input.md | 145 + frontend/functions/ui/date_picker_input.tsx | 17 + frontend/functions/ui/detail_page.md | 4 +- frontend/functions/ui/detail_page.tsx | 137 +- frontend/functions/ui/dialog.md | 12 +- frontend/functions/ui/dialog.tsx | 177 +- frontend/functions/ui/dropdown_menu.md | 6 +- frontend/functions/ui/dropdown_menu.tsx | 284 +- frontend/functions/ui/dropzone.md | 118 + frontend/functions/ui/dropzone.tsx | 20 + frontend/functions/ui/empty_state.md | 103 + frontend/functions/ui/empty_state.tsx | 55 + frontend/functions/ui/error_page.md | 75 + frontend/functions/ui/error_page.tsx | 56 + frontend/functions/ui/file_input.md | 102 + frontend/functions/ui/file_input.tsx | 10 + frontend/functions/ui/form_field.md | 4 +- frontend/functions/ui/form_field.tsx | 33 +- frontend/functions/ui/graph/index.tsx | 54 +- frontend/functions/ui/index.ts | 29 +- frontend/functions/ui/indicator.md | 77 + frontend/functions/ui/indicator.tsx | 39 + frontend/functions/ui/input.md | 10 +- frontend/functions/ui/input.tsx | 45 +- frontend/functions/ui/kpi_card.md | 2 +- frontend/functions/ui/kpi_card.tsx | 95 +- frontend/functions/ui/label.md | 8 +- frontend/functions/ui/label.tsx | 17 +- frontend/functions/ui/line_chart.md | 14 +- frontend/functions/ui/line_chart.tsx | 59 +- frontend/functions/ui/loading_overlay.md | 58 + frontend/functions/ui/loading_overlay.tsx | 26 + frontend/functions/ui/mantine_provider.md | 59 + frontend/functions/ui/mantine_provider.tsx | 29 + frontend/functions/ui/multi_select.md | 111 + frontend/functions/ui/multi_select.tsx | 10 + frontend/functions/ui/nav_link.md | 80 + frontend/functions/ui/nav_link.tsx | 44 + frontend/functions/ui/number_input.md | 92 + frontend/functions/ui/number_input.tsx | 48 + frontend/functions/ui/page_header.md | 4 +- frontend/functions/ui/page_header.tsx | 104 +- frontend/functions/ui/pagination.md | 72 +- frontend/functions/ui/pagination.tsx | 127 +- frontend/functions/ui/password_input.md | 109 + frontend/functions/ui/password_input.tsx | 10 + frontend/functions/ui/pie_chart.md | 20 +- frontend/functions/ui/pie_chart.tsx | 75 +- frontend/functions/ui/pin_input.md | 105 + frontend/functions/ui/pin_input.tsx | 10 + frontend/functions/ui/popover.md | 12 +- frontend/functions/ui/popover.tsx | 117 +- frontend/functions/ui/progress_bar.md | 13 +- frontend/functions/ui/progress_bar.tsx | 109 +- frontend/functions/ui/radio_group.md | 10 +- frontend/functions/ui/radio_group.tsx | 95 +- frontend/functions/ui/rating.md | 94 + frontend/functions/ui/rating.tsx | 10 + frontend/functions/ui/ring_progress.md | 67 + frontend/functions/ui/ring_progress.tsx | 32 + frontend/functions/ui/search_bar.md | 4 +- frontend/functions/ui/search_bar.tsx | 48 +- frontend/functions/ui/segmented_control.md | 71 + frontend/functions/ui/segmented_control.tsx | 39 + frontend/functions/ui/select.md | 97 +- frontend/functions/ui/select.tsx | 93 +- frontend/functions/ui/settings_page.md | 4 +- frontend/functions/ui/settings_page.tsx | 129 +- frontend/functions/ui/sheet.md | 12 +- frontend/functions/ui/sheet.tsx | 195 +- frontend/functions/ui/simple_select.md | 4 +- frontend/functions/ui/simple_select.tsx | 86 +- frontend/functions/ui/skeleton.md | 14 +- frontend/functions/ui/skeleton.tsx | 52 +- frontend/functions/ui/slider.md | 122 + frontend/functions/ui/slider.tsx | 16 + frontend/functions/ui/sparkline.md | 2 +- frontend/functions/ui/sparkline.tsx | 76 +- frontend/functions/ui/stepper.md | 67 + frontend/functions/ui/stepper.tsx | 45 + frontend/functions/ui/switch_toggle.md | 10 +- frontend/functions/ui/switch_toggle.tsx | 75 +- frontend/functions/ui/tabs.md | 8 +- frontend/functions/ui/tabs.tsx | 78 +- frontend/functions/ui/tags_input.md | 97 + frontend/functions/ui/tags_input.tsx | 10 + frontend/functions/ui/textarea.md | 12 +- frontend/functions/ui/textarea.tsx | 43 +- frontend/functions/ui/theme_provider.md | 66 - frontend/functions/ui/theme_provider.tsx | 101 - frontend/functions/ui/timeline.md | 58 + frontend/functions/ui/timeline.tsx | 45 + frontend/functions/ui/toast.md | 8 +- frontend/functions/ui/toast.tsx | 205 +- frontend/functions/ui/tooltip.md | 15 +- frontend/functions/ui/tooltip.tsx | 98 +- frontend/package.json | 26 +- frontend/pnpm-lock.yaml | 3240 ++--------------- frontend/postcss.config.cjs | 14 + frontend/src/components/ui/button.tsx | 58 - frontend/src/globals.css | 133 +- frontend/src/lib/utils.ts | 6 - frontend/types/core/component_variants.ts | 6 - frontend/vite.config.ts | 6 +- 163 files changed, 6008 insertions(+), 6310 deletions(-) delete mode 100644 frontend/components.json delete mode 100644 frontend/functions/core/cn.md delete mode 100644 frontend/functions/core/cn.ts delete mode 100644 frontend/functions/core/generate_theme_css.md delete mode 100644 frontend/functions/core/generate_theme_css.ts delete mode 100644 frontend/functions/core/get_theme_tokens.md delete mode 100644 frontend/functions/core/get_theme_tokens.ts delete mode 100644 frontend/functions/core/theme_config_to_colors.md delete mode 100644 frontend/functions/core/theme_config_to_colors.ts create mode 100644 frontend/functions/ui/action_icon.md create mode 100644 frontend/functions/ui/action_icon.tsx create mode 100644 frontend/functions/ui/app_shell.md create mode 100644 frontend/functions/ui/app_shell.tsx delete mode 100644 frontend/functions/ui/apply_theme.md delete mode 100644 frontend/functions/ui/apply_theme.tsx create mode 100644 frontend/functions/ui/auth_form.md create mode 100644 frontend/functions/ui/auth_form.tsx create mode 100644 frontend/functions/ui/autocomplete.md create mode 100644 frontend/functions/ui/autocomplete.tsx create mode 100644 frontend/functions/ui/chip.md create mode 100644 frontend/functions/ui/chip.tsx create mode 100644 frontend/functions/ui/color_input.md create mode 100644 frontend/functions/ui/color_input.tsx create mode 100644 frontend/functions/ui/date_picker_input.md create mode 100644 frontend/functions/ui/date_picker_input.tsx create mode 100644 frontend/functions/ui/dropzone.md create mode 100644 frontend/functions/ui/dropzone.tsx create mode 100644 frontend/functions/ui/empty_state.md create mode 100644 frontend/functions/ui/empty_state.tsx create mode 100644 frontend/functions/ui/error_page.md create mode 100644 frontend/functions/ui/error_page.tsx create mode 100644 frontend/functions/ui/file_input.md create mode 100644 frontend/functions/ui/file_input.tsx create mode 100644 frontend/functions/ui/indicator.md create mode 100644 frontend/functions/ui/indicator.tsx create mode 100644 frontend/functions/ui/loading_overlay.md create mode 100644 frontend/functions/ui/loading_overlay.tsx create mode 100644 frontend/functions/ui/mantine_provider.md create mode 100644 frontend/functions/ui/mantine_provider.tsx create mode 100644 frontend/functions/ui/multi_select.md create mode 100644 frontend/functions/ui/multi_select.tsx create mode 100644 frontend/functions/ui/nav_link.md create mode 100644 frontend/functions/ui/nav_link.tsx create mode 100644 frontend/functions/ui/number_input.md create mode 100644 frontend/functions/ui/number_input.tsx create mode 100644 frontend/functions/ui/password_input.md create mode 100644 frontend/functions/ui/password_input.tsx create mode 100644 frontend/functions/ui/pin_input.md create mode 100644 frontend/functions/ui/pin_input.tsx create mode 100644 frontend/functions/ui/rating.md create mode 100644 frontend/functions/ui/rating.tsx create mode 100644 frontend/functions/ui/ring_progress.md create mode 100644 frontend/functions/ui/ring_progress.tsx create mode 100644 frontend/functions/ui/segmented_control.md create mode 100644 frontend/functions/ui/segmented_control.tsx create mode 100644 frontend/functions/ui/slider.md create mode 100644 frontend/functions/ui/slider.tsx create mode 100644 frontend/functions/ui/stepper.md create mode 100644 frontend/functions/ui/stepper.tsx create mode 100644 frontend/functions/ui/tags_input.md create mode 100644 frontend/functions/ui/tags_input.tsx delete mode 100644 frontend/functions/ui/theme_provider.md delete mode 100644 frontend/functions/ui/theme_provider.tsx create mode 100644 frontend/functions/ui/timeline.md create mode 100644 frontend/functions/ui/timeline.tsx create mode 100644 frontend/postcss.config.cjs delete mode 100644 frontend/src/components/ui/button.tsx delete mode 100644 frontend/src/lib/utils.ts diff --git a/.claude/commands/frontend.md b/.claude/commands/frontend.md index 2678b3a6..d9ad9df0 100644 --- a/.claude/commands/frontend.md +++ b/.claude/commands/frontend.md @@ -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 ( - - {/* Router y contenido aqui */} - - ) -} +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( + + + + + + , +) ``` ### 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: ` + )} - - + + {/* Table */} -
- - - + +
+ + {columns.map((col) => ( - + ))} {(onEdit || onDelete) && ( - + Actions )} - - - + + + {data.length === 0 ? ( - - - + + +
+ No items yet. +
+
+
) : ( data.map((row, i) => ( - + {columns.map((col) => ( - + ))} {(onEdit || onDelete) && ( - + + )} - + )) )} - -
+ {col.label} - Actions
- No items yet. -
+ {col.render ? col.render(row[col.key], row) : String(row[col.key] ?? '')} - -
+ + {onEdit && ( - + onEdit(row)}> + + )} {onDelete && ( - + onDelete(row)}> + + )} -
-
-
+ + + - {/* Form fields definition (for agent use — renders a form preview) */} -
-
+ {/* Form fields definition (for agent use) */} +
+ ) } diff --git a/frontend/functions/ui/dashboard_layout.md b/frontend/functions/ui/dashboard_layout.md index 947a8c36..8a8f2945 100644 --- a/frontend/functions/ui/dashboard_layout.md +++ b/frontend/functions/ui/dashboard_layout.md @@ -8,12 +8,12 @@ purity: pure signature: "dashboardLayout(props: DashboardLayoutProps): ReactElement" description: "Genera un grid responsive de dashboard a partir de un array de widgets con span configurable. 1-4 columnas con auto-responsive." tags: [dashboard, layout, grid, 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 de layout: número de columnas y array de widgets con id, título, contenido y span" diff --git a/frontend/functions/ui/dashboard_layout.tsx b/frontend/functions/ui/dashboard_layout.tsx index a3d3b89e..d542653c 100644 --- a/frontend/functions/ui/dashboard_layout.tsx +++ b/frontend/functions/ui/dashboard_layout.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { cn } from '../core/cn' +import { SimpleGrid, Paper, Text } from '@mantine/core' interface DashboardWidget { id: string @@ -16,51 +16,37 @@ interface DashboardLayoutProps { className?: string } -const gapClasses = { sm: 'gap-2', md: 'gap-4', lg: 'gap-6' } - -const spanClasses: Record = { - 1: 'col-span-1', - 2: 'col-span-1 md:col-span-2', - 3: 'col-span-1 md:col-span-2 lg:col-span-3', - 4: 'col-span-1 md:col-span-2 lg:col-span-4', -} - -const rowSpanClasses: Record = { - 1: 'row-span-1', - 2: 'row-span-2', -} +const gapMap = { sm: 'xs', md: 'md', lg: 'lg' } as const export function dashboardLayout({ widgets, columns = 4, gap = 'md', - className, }: DashboardLayoutProps): React.ReactElement { - const gridCols: Record = { - 1: 'grid-cols-1', - 2: 'grid-cols-1 md:grid-cols-2', - 3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3', - 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4', - } - return ( -
+ {widgets.map((widget) => ( -
1 ? `span ${widget.span}` : undefined, + gridRow: widget.rowSpan === 2 ? 'span 2' : undefined, + }} > {widget.title && ( -

{widget.title}

+ {widget.title} )} {widget.content} -
+ ))} -
+ ) } diff --git a/frontend/functions/ui/data_table.md b/frontend/functions/ui/data_table.md index f6b2440b..c10e82f2 100644 --- a/frontend/functions/ui/data_table.md +++ b/frontend/functions/ui/data_table.md @@ -8,12 +8,12 @@ purity: impure signature: "DataTable(props: DataTableProps): JSX.Element" description: "Tabla de datos con sticky header, overflow scroll, heatmap por columna, formato condicional (number/datetime/currency) y hover rows. Auto-detecta columnas desde la primera fila si no se proveen." tags: [table, data, heatmap, dashboard, component, ui, format, visualization] -uses_functions: [cn_ts_core] +uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" -imports: [react] +imports: [react, "@mantine/core"] output: "Componente DataTable que renderiza tabla con sticky header, heatmap condicional y formato automático de datos" tested: false tests: [] @@ -44,10 +44,6 @@ props: type: "Error | null" required: false description: "Error a mostrar si la carga falló." - - name: className - type: "string" - required: false - description: "Clases CSS adicionales." emits: [] has_state: false framework: react diff --git a/frontend/functions/ui/data_table.tsx b/frontend/functions/ui/data_table.tsx index f76dbe49..385708ea 100644 --- a/frontend/functions/ui/data_table.tsx +++ b/frontend/functions/ui/data_table.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { cn } from '../core/cn' +import { Table, Text, Center, Loader } from '@mantine/core' interface ColumnDef { key: string @@ -16,7 +16,6 @@ interface DataTableProps { /** Column keys that should be colored by value intensity (heatmap). */ heatmapColumns?: string[] maxHeight?: number | string - className?: string loading?: boolean error?: Error | null } @@ -33,7 +32,7 @@ function formatCell(value: unknown, format?: string): string { if (!isNaN(num)) { if (format.includes('f')) { const match = format.match(/\.(\d+)f/) - const d = match ? parseInt(match[1]) : 0 + const d = match ? parseInt(match[1]!) : 0 let str = num.toFixed(d) if (format.includes(',')) { str = Number(str).toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d }) @@ -51,7 +50,6 @@ function DataTableComponent({ columns, heatmapColumns = [], maxHeight = 500, - className, loading = false, error = null, }: DataTableProps) { @@ -59,7 +57,7 @@ function DataTableComponent({ const effectiveColumns: ColumnDef[] = (columns && columns.length > 0) ? columns : (data && data.length > 0) - ? Object.keys(data[0]).map(k => ({ key: k, label: k })) + ? Object.keys(data[0]!).map(k => ({ key: k, label: k })) : [] // Compute heatmap ranges per column @@ -82,73 +80,74 @@ function DataTableComponent({ const num = Number(value) if (isNaN(num)) return undefined const t = (num - range.min) / (range.max - range.min) - // Dark blue (low) → bright blue (high) const alpha = 0.1 + t * 0.55 return { backgroundColor: `rgba(59, 130, 246, ${alpha})` } } - const maxHeightStyle = typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight - if (loading && (!data || data.length === 0)) { return ( -
- Loading... -
+
+ +
) } if (error) { return ( -
- {error.message} -
+
+ {error.message} +
) } return ( -
- - - + +
+ + {effectiveColumns.map(col => ( - + ))} - - - + + + {(data ?? []).map((row, i) => ( - + {effectiveColumns.map(col => { const align = col.align ?? (typeof row[col.key] === 'number' ? 'right' : 'left') return ( - + ) })} - + ))} - -
{col.label} -
{formatCell(row[col.key], col.format)} -
+ + {(!data || data.length === 0) && ( -

No data

+
+ No data +
)} -
+ ) } diff --git a/frontend/functions/ui/date_picker_input.md b/frontend/functions/ui/date_picker_input.md new file mode 100644 index 00000000..fde1c8d5 --- /dev/null +++ b/frontend/functions/ui/date_picker_input.md @@ -0,0 +1,145 @@ +--- +name: date_picker_input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "DatePickerInput(props: DatePickerInputProps): JSX.Element" +description: "Selector de fecha con input y calendario desplegable. Soporta fecha simple, múltiple y rango. Wrapper sobre Mantine DatePickerInput." +tags: [date, picker, calendar, form, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/dates"] +output: "Componente DatePickerInput que renderiza input con calendario para selección de fechas" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/date_picker_input.tsx" +props: + - name: type + type: "'default' | 'multiple' | 'range'" + required: false + description: "Modo de selección — fecha simple, múltiples fechas, o rango de fechas" + - name: value + type: "DateValue | DateValue[] | [DateValue, DateValue] | null" + required: false + description: "Fecha o fechas seleccionadas (controlled)" + - name: onChange + type: "(value: DateValue | DateValue[] | [DateValue, DateValue] | null) => void" + required: false + description: "Callback al cambiar la selección de fecha" + - name: valueFormat + type: "string" + required: false + description: "Formato de la fecha mostrada en el input (ej: 'DD/MM/YYYY')" + - name: clearable + type: "boolean" + required: false + description: "Permite limpiar la selección de fecha" + - name: label + type: "string" + required: false + description: "Label del campo" + - name: placeholder + type: "string" + required: false + description: "Texto cuando no hay fecha seleccionada" + - name: minDate + type: "Date" + required: false + description: "Fecha mínima seleccionable" + - name: maxDate + type: "Date" + required: false + description: "Fecha máxima seleccionable" + - name: disabled + type: "boolean" + required: false + description: "Deshabilitar el selector" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño del componente" +emits: [onChange] +has_state: true +framework: react +variant: [default] +params: + - name: props + desc: "Props del componente DatePickerInput — incluye type (modo de selección), value, onChange, valueFormat, clearable, label, placeholder, minDate, maxDate, disabled y size" +--- + +## Ejemplo + +```tsx +import { DatePickerInput, DatePicker } from '@fn_library' +import { useState } from 'react' + +// Fecha simple +function SingleDateExample() { + const [value, setValue] = useState(null) + return ( + + ) +} + +// Rango de fechas +function RangeDateExample() { + const [range, setRange] = useState<[Date | null, Date | null]>([null, null]) + return ( + + ) +} + +// Múltiples fechas +function MultipleDateExample() { + const [dates, setDates] = useState([]) + return ( + + ) +} + +// DatePicker inline (sin input) +function InlineDateExample() { + const [value, setValue] = useState(null) + return ( + + ) +} +``` + +## Notas + +- Wrapper directo sobre `DatePickerInput` y `DatePicker` de `@mantine/dates` v9. Todas las props de Mantine son válidas. +- Requiere importar `@mantine/dates/styles.css` — este wrapper ya lo incluye. +- El prop `type` controla el modo: `'default'` (fecha simple), `'multiple'` (varias fechas), `'range'` (rango con inicio y fin). +- `DatePicker` es el calendario inline sin input — útil para formularios donde el calendario debe estar siempre visible. +- `valueFormat` acepta tokens de dayjs (ej: `'DD/MM/YYYY'`, `'MMMM D, YYYY'`). +- Re-exporta también `DatePicker` de `@mantine/dates` con el mismo patrón de wrapper. diff --git a/frontend/functions/ui/date_picker_input.tsx b/frontend/functions/ui/date_picker_input.tsx new file mode 100644 index 00000000..f69bf0cd --- /dev/null +++ b/frontend/functions/ui/date_picker_input.tsx @@ -0,0 +1,17 @@ +import { DatePickerInput as MantineDatePickerInput, DatePicker as MantineDatePicker } from '@mantine/dates' +import type { DatePickerInputProps as MantineDatePickerInputProps, DatePickerProps as MantineDatePickerProps } from '@mantine/dates' +import '@mantine/dates/styles.css' + +interface DatePickerInputProps extends MantineDatePickerInputProps<'default'> {} +interface DatePickerProps extends MantineDatePickerProps<'default'> {} + +function DatePickerInput(props: DatePickerInputProps) { + return +} + +function DatePicker(props: DatePickerProps) { + return +} + +export { DatePickerInput, DatePicker } +export type { DatePickerInputProps, DatePickerProps } diff --git a/frontend/functions/ui/detail_page.md b/frontend/functions/ui/detail_page.md index 00dab53a..49b1acd9 100644 --- a/frontend/functions/ui/detail_page.md +++ b/frontend/functions/ui/detail_page.md @@ -8,12 +8,12 @@ purity: pure signature: "detailPage(props: DetailPageProps): ReactElement" description: "Genera una página de detalle de entidad con header (avatar, badge, back), grid de campos, tabs con contadores y timeline de actividad." tags: [detail, page, entity, timeline, factory, composition, ui] -uses_functions: [cn_ts_core] +uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" -imports: [react] +imports: [react, "@mantine/core", "@tabler/icons-react"] params: - name: props desc: "Configuración de página de detalle: título, avatar, badge, tabs, timeline y campos de metadata" diff --git a/frontend/functions/ui/detail_page.tsx b/frontend/functions/ui/detail_page.tsx index 86bae0d4..e8d577a5 100644 --- a/frontend/functions/ui/detail_page.tsx +++ b/frontend/functions/ui/detail_page.tsx @@ -1,5 +1,6 @@ import * as React from 'react' -import { cn } from '../core/cn' +import { Stack, Group, Title, Text, ActionIcon, Box, Tabs, Badge, Timeline, SimpleGrid } from '@mantine/core' +import { IconChevronLeft } from '@tabler/icons-react' interface DetailField { label: string @@ -38,96 +39,98 @@ interface DetailPageProps { className?: string } -const variantDotColors = { - default: 'bg-primary', - success: 'bg-green-500', - warning: 'bg-amber-500', - error: 'bg-red-500', +const variantColors: Record = { + default: 'blue', + success: 'green', + warning: 'yellow', + error: 'red', } export function detailPage({ title, subtitle, badge, avatar, actions, onBack, - fields, tabs, activeTab, onTabChange, timeline, className, + fields, tabs, activeTab, onTabChange, timeline, }: DetailPageProps): React.ReactElement { return ( -
+ {/* Header */} -
-
+ + {onBack && ( - + + + )} - {avatar &&
{avatar}
} -
-
-

{title}

+ {avatar && ( + + {avatar} + + )} + + + {title} {badge} -
- {subtitle &&

{subtitle}

} -
-
- {actions &&
{actions}
} -
+ + {subtitle && {subtitle}} +
+ + {actions && {actions}} + {/* Fields grid */} -
+ {fields.map((field, i) => ( -
-

{field.label}

-
{field.value}
-
+ + + {field.label} + {field.value} + + ))} -
+ {/* Tabs */} {tabs && tabs.length > 0 && ( -
- + + v && onTabChange?.(v)}> + + {tabs.map((tab) => ( + {tab.count} : undefined} + > + {tab.label} + + ))} + + {tabs.find(t => t.value === activeTab)?.content} -
+ )} {/* Timeline */} {timeline && timeline.length > 0 && ( -
-

Activity

-
- {timeline.map((event, i) => ( -
-
-
- {i < timeline.length - 1 &&
} -
-
-

{event.title}

- {event.description &&

{event.description}

} -

{event.timestamp}

-
-
+ + Activity + + {timeline.map((event) => ( + {event.title}} + > + {event.description && {event.description}} + {event.timestamp} + ))} -
-
+ + )} -
+ ) } diff --git a/frontend/functions/ui/dialog.md b/frontend/functions/ui/dialog.md index ae84dc09..1d7a0bf4 100644 --- a/frontend/functions/ui/dialog.md +++ b/frontend/functions/ui/dialog.md @@ -6,15 +6,15 @@ domain: ui version: "1.0.0" purity: impure signature: "Dialog(props: DialogRootProps): JSX.Element" -description: "Diálogo modal accesible con overlay blur, animaciones, close button y sistema de slots (header, footer, title, description)." -tags: [dialog, modal, overlay, component, ui, interactive] -uses_functions: [cn_ts_core] +description: "Diálogo modal accesible con close button y sistema de slots (header, footer, title, description). Mantine Modal." +tags: [dialog, modal, overlay, component, ui, interactive, mantine] +uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" -imports: ["@base-ui/react", lucide-react, react] -output: "Componente Dialog que renderiza modal accesible con overlay blur, focus trap y sistema de slots composables" +imports: ["@mantine/core", react] +output: "Componente Dialog que renderiza modal accesible con focus trap y sistema de slots composables via Mantine Modal" tested: false tests: [] test_file_path: "" @@ -53,4 +53,4 @@ source_file: "frontend/src/components/ui/dialog.tsx" ## Notas -10 subcomponentes exportados. Base-UI Dialog primitive para accesibilidad completa (focus trap, escape, click outside). +10 subcomponentes exportados. Mantine Modal para accesibilidad completa (focus trap, escape, click outside). DialogPortal y DialogOverlay son no-ops mantenidos por compatibilidad. diff --git a/frontend/functions/ui/dialog.tsx b/frontend/functions/ui/dialog.tsx index 77c7d8fa..ef22cde4 100644 --- a/frontend/functions/ui/dialog.tsx +++ b/frontend/functions/ui/dialog.tsx @@ -1,73 +1,134 @@ -import * as React from "react" -import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" -import { cn } from "../core/cn" -import { XIcon } from "lucide-react" +import * as React from 'react' +import { Modal, Box, Text, Group } from '@mantine/core' -function Dialog({ ...props }: DialogPrimitive.Root.Props) { - return +interface DialogProps { + open?: boolean + onOpenChange?: (open: boolean) => void + children: React.ReactNode } -function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { - return -} +const DialogContext = React.createContext<{ + open: boolean + setOpen: (open: boolean) => void +}>({ open: false, setOpen: () => {} }) -function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { - return -} - -function DialogClose({ ...props }: DialogPrimitive.Close.Props) { - return -} - -function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) { - return ( - +function Dialog({ open: controlledOpen, onOpenChange, children }: DialogProps) { + const [internalOpen, setInternalOpen] = React.useState(false) + const open = controlledOpen ?? internalOpen + const setOpen = React.useCallback( + (v: boolean) => { + onOpenChange?.(v) + if (controlledOpen === undefined) setInternalOpen(v) + }, + [controlledOpen, onOpenChange], ) -} - -function DialogContent({ className, children, showCloseButton = true, ...props }: DialogPrimitive.Popup.Props & { showCloseButton?: boolean }) { return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ) -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return
-} - -function DialogFooter({ className, children, ...props }: React.ComponentProps<"div">) { - return ( -
+ {children} -
+ ) } -function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { - return +function DialogTrigger({ children, ...props }: React.ComponentProps<'button'>) { + const { setOpen } = React.useContext(DialogContext) + return ( + + ) } -function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) { - return +function DialogPortal({ children }: { children: React.ReactNode }) { + return <>{children} +} + +function DialogClose({ children, ...props }: React.ComponentProps<'button'>) { + const { setOpen } = React.useContext(DialogContext) + return ( + + ) +} + +function DialogOverlay() { + return null +} + +function DialogContent({ + children, + showCloseButton = true, + className, + ...props +}: React.ComponentProps<'div'> & { showCloseButton?: boolean }) { + const { open, setOpen } = React.useContext(DialogContext) + return ( + setOpen(false)} + withCloseButton={showCloseButton} + radius="md" + padding="md" + size="sm" + centered + data-slot="dialog-content" + className={className} + {...props} + > + {children} + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { + return +} + +function DialogFooter({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( + + {children} + + ) +} + +function DialogTitle({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( + + {children} + + ) +} + +function DialogDescription({ className, children, ...props }: React.ComponentProps<'div'>) { + return ( + + {children} + + ) } export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } diff --git a/frontend/functions/ui/dropdown_menu.md b/frontend/functions/ui/dropdown_menu.md index b68da7ee..4f0be746 100644 --- a/frontend/functions/ui/dropdown_menu.md +++ b/frontend/functions/ui/dropdown_menu.md @@ -7,13 +7,13 @@ version: "1.0.0" purity: impure signature: "DropdownMenu(props: DropdownMenuProps): JSX.Element" description: "Menu de acciones y contexto accesible con items, checkboxes, radios, separadores y submenus. Base-UI Menu primitive." -tags: [dropdown, menu, component, ui, interactive, overlay, base-ui] -uses_functions: [cn_ts_core] +tags: [dropdown, menu, component, ui, interactive, overlay, mantine] +uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" -imports: ["@base-ui/react/menu", "lucide-react"] +imports: ["@mantine/core"] output: "Componente DropdownMenu que renderiza menú desplegable accesible con items, checkboxes, radios y submenus" tested: false tests: [] diff --git a/frontend/functions/ui/dropdown_menu.tsx b/frontend/functions/ui/dropdown_menu.tsx index 3fb24bea..5b755af4 100644 --- a/frontend/functions/ui/dropdown_menu.tsx +++ b/frontend/functions/ui/dropdown_menu.tsx @@ -1,187 +1,125 @@ -import * as React from "react" -import { Menu as MenuPrimitive } from "@base-ui/react/menu" -import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" -import { cn } from "../core/cn" +import * as React from 'react' +import { Menu, Text } from '@mantine/core' -function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { - return -} - -function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { - return -} - -function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { - return -} - -function DropdownMenuContent({ className, sideOffset = 4, ...props }: MenuPrimitive.Positioner.Props) { +function DropdownMenu({ children, ...props }: { children: React.ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; modal?: boolean }) { return ( - - - - {props.children} - - - - ) -} - -function DropdownMenuItem({ className, inset, ...props }: MenuPrimitive.Item.Props & { inset?: boolean }) { - return ( - - ) -} - -function DropdownMenuCheckboxItem({ className, children, checked, ...props }: MenuPrimitive.CheckboxItem.Props) { - return ( - - - - - - - {children} - - ) -} - -function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { - return -} - -function DropdownMenuRadioItem({ className, children, ...props }: MenuPrimitive.RadioItem.Props) { - return ( - - - - - - - {children} - - ) -} - -function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { - return -} - -function DropdownMenuLabel({ className, inset, ...props }: MenuPrimitive.GroupLabel.Props & { inset?: boolean }) { - return ( - - ) -} - -function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { - return ( - - ) -} - -function DropdownMenuSub({ ...props }: MenuPrimitive.Root.Props) { - return -} - -function DropdownMenuSubTrigger({ className, inset, children, ...props }: MenuPrimitive.SubmenuTrigger.Props & { inset?: boolean }) { - return ( - {children} - - + ) } -function DropdownMenuSubContent({ className, ...props }: MenuPrimitive.Positioner.Props) { +function DropdownMenuTrigger({ children, ...props }: { children: React.ReactNode; asChild?: boolean; className?: string }) { + return {children} +} + +function DropdownMenuPortal({ children }: { children?: React.ReactNode }) { + return <>{children} +} + +function DropdownMenuContent({ children, className }: { children?: React.ReactNode; className?: string; sideOffset?: number }) { + return {children} +} + +function DropdownMenuItem({ children, className, inset, ...props }: { + children?: React.ReactNode + className?: string + inset?: boolean + onClick?: () => void + onActivate?: () => void + disabled?: boolean +}) { return ( - - - - {props.children} - - - + + {children} + ) } +function DropdownMenuCheckboxItem({ children, className, checked, onCheckedChange, ...props }: { + children?: React.ReactNode + className?: string + checked?: boolean + onCheckedChange?: (checked: boolean) => void + disabled?: boolean +}) { + return ( + onCheckedChange?.(!checked)} + disabled={props.disabled} + leftSection={checked ? : } + > + {children} + + ) +} + +function DropdownMenuRadioGroup({ children }: { children?: React.ReactNode; value?: string; onValueChange?: (value: string) => void }) { + return <>{children} +} + +function DropdownMenuRadioItem({ children, className, value, ...props }: { + children?: React.ReactNode + className?: string + value?: string + disabled?: boolean + onClick?: () => void +}) { + return ( + + {children} + + ) +} + +function DropdownMenuGroup({ children }: { children?: React.ReactNode }) { + return <>{children} +} + +function DropdownMenuLabel({ children, className, inset }: { children?: React.ReactNode; className?: string; inset?: boolean }) { + return ( + + {children} + + ) +} + +function DropdownMenuSeparator({ className }: { className?: string }) { + return +} + +function DropdownMenuShortcut({ children, className }: { children?: React.ReactNode; className?: string }) { + return {children} +} + +function DropdownMenuSub({ children }: { children?: React.ReactNode }) { + return <>{children} +} + +function DropdownMenuSubTrigger({ children, className, inset }: { children?: React.ReactNode; className?: string; inset?: boolean }) { + return ( + + {children} + + ) +} + +function DropdownMenuSubContent({ children, className }: { children?: React.ReactNode; className?: string }) { + return {children} +} + export { DropdownMenu, DropdownMenuCheckboxItem, diff --git a/frontend/functions/ui/dropzone.md b/frontend/functions/ui/dropzone.md new file mode 100644 index 00000000..a3e8037e --- /dev/null +++ b/frontend/functions/ui/dropzone.md @@ -0,0 +1,118 @@ +--- +name: dropzone +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "Dropzone(props: DropzoneProps): JSX.Element" +description: "Zona de drag-and-drop para archivos con estados idle/accept/reject, límite de tamaño y tipos MIME. Wrapper sobre Mantine Dropzone." +tags: [dropzone, upload, drag-drop, file, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/dropzone"] +output: "Componente Dropzone que renderiza área de arrastrar y soltar archivos con feedback visual" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/dropzone.tsx" +props: + - name: onDrop + type: "(files: File[]) => void" + required: false + description: "Callback ejecutado cuando el usuario suelta archivos aceptados" + - name: onReject + type: "(files: FileRejection[]) => void" + required: false + description: "Callback ejecutado cuando el usuario suelta archivos rechazados (tipo o tamaño inválido)" + - name: accept + type: "Record" + required: false + description: "Tipos MIME aceptados. Usar IMAGE_MIME_TYPE o MIME_TYPES para constantes predefinidas" + - name: maxSize + type: "number" + required: false + description: "Tamaño máximo de archivo en bytes" + - name: multiple + type: "boolean" + required: false + description: "Permite seleccionar múltiples archivos a la vez" + - name: loading + type: "boolean" + required: false + description: "Muestra estado de carga y desactiva la interacción" + - name: disabled + type: "boolean" + required: false + description: "Desactiva el dropzone" + - name: children + type: "React.ReactNode" + required: true + description: "Contenido interno, generalmente compuesto con DropzoneAccept, DropzoneReject y DropzoneIdle" +emits: [onDrop, onReject] +has_state: true +framework: react +variant: [] +--- + +## Ejemplo + +```tsx +import { + Dropzone, + DropzoneAccept, + DropzoneReject, + DropzoneIdle, + IMAGE_MIME_TYPE, +} from '@fn_library/dropzone' +import { IconPhoto, IconUpload, IconX } from '@tabler/icons-react' +import { Group, Text } from '@mantine/core' + +function ImageUploader() { + return ( + console.log('Archivos aceptados:', files)} + onReject={(files) => console.log('Archivos rechazados:', files)} + accept={IMAGE_MIME_TYPE} + maxSize={5 * 1024 ** 2} + > + + + + + + + + + + +
+ + Arrastra imágenes aquí o haz clic para seleccionar + + + Máximo 5 MB por imagen + +
+
+
+ ) +} +``` + +## Notas + +El prop `onDrop` tiene un default vacío (`() => {}`) para que el componente sea válido sin handler. Siempre sobreescribirlo en uso real. + +Sub-componentes exportados: +- `DropzoneAccept` — visible cuando el archivo arrastrado es aceptado (tipo y tamaño válidos) +- `DropzoneReject` — visible cuando el archivo es rechazado +- `DropzoneIdle` — visible en estado de reposo +- `DropzoneFullScreen` — captura drops en cualquier parte de la pantalla + +Constantes de tipos MIME exportadas: +- `IMAGE_MIME_TYPE` — imágenes comunes (png, jpg, gif, webp, etc.) +- `MIME_TYPES` — objeto con claves por tipo (pdf, csv, xlsx, mp4, etc.) diff --git a/frontend/functions/ui/dropzone.tsx b/frontend/functions/ui/dropzone.tsx new file mode 100644 index 00000000..a46eedec --- /dev/null +++ b/frontend/functions/ui/dropzone.tsx @@ -0,0 +1,20 @@ +import { Dropzone as MantineDropzone, IMAGE_MIME_TYPE, MIME_TYPES } from '@mantine/dropzone' +import type { DropzoneProps as MantineDropzoneProps } from '@mantine/dropzone' +import '@mantine/dropzone/styles.css' + +interface DropzoneProps extends Partial { + children: React.ReactNode +} + +function Dropzone({ children, ...props }: DropzoneProps) { + return {}} {...props}>{children} +} + +// Re-export sub-components and constants +const DropzoneAccept = MantineDropzone.Accept +const DropzoneReject = MantineDropzone.Reject +const DropzoneIdle = MantineDropzone.Idle +const DropzoneFullScreen = MantineDropzone.FullScreen + +export { Dropzone, DropzoneAccept, DropzoneReject, DropzoneIdle, DropzoneFullScreen, IMAGE_MIME_TYPE, MIME_TYPES } +export type { DropzoneProps } diff --git a/frontend/functions/ui/empty_state.md b/frontend/functions/ui/empty_state.md new file mode 100644 index 00000000..5f8fd703 --- /dev/null +++ b/frontend/functions/ui/empty_state.md @@ -0,0 +1,103 @@ +--- +name: empty_state +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "EmptyState(props: EmptyStateProps): JSX.Element" +description: "Placeholder para listas y tablas vacías con icono, título, descripción y acción opcional. Tabler Icons por defecto." +tags: [empty-state, placeholder, no-data, component, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core", "@tabler/icons-react"] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/empty_state.tsx" +framework: react +has_state: false +emits: [onAction] +props: + - name: icon + type: "React.ReactNode" + required: false + description: "Icono a mostrar. Default: IconInbox de @tabler/icons-react." + - name: title + type: "string" + required: false + description: "Título del empty state. Default: 'No data found'." + - name: description + type: "string" + required: false + description: "Descripción explicativa. Default: 'There are no items to display yet.'." + - name: actionLabel + type: "string" + required: false + description: "Texto del botón de acción. Se muestra solo si también hay onAction." + - name: onAction + type: "() => void" + required: false + description: "Callback del botón de acción. Se muestra solo si también hay actionLabel." + - name: size + type: "MantineSize" + required: false + description: "Tamaño general del componente. Afecta el icono, texto y botón. Default: 'md'." + - name: children + type: "React.ReactNode" + required: false + description: "Contenido custom renderizado debajo de la descripción y antes del botón." +output: "Componente EmptyState centrado con icono, mensaje y botón de acción para estados sin datos" +params: + - name: props + desc: "Props del componente EmptyState" +--- + +## Ejemplo + +```tsx +import { EmptyState } from '@fn_library/empty_state' + +// Default — sin datos + + +// Con acción + navigate('/new')} +/> + +// Con icono custom +import { IconDatabase } from '@tabler/icons-react' + +} + title="No databases connected" + description="Connect a database to start querying data." + size="lg" +/> + +// Dentro de una Card +import { Card } from '@mantine/core' +import { EmptyState } from '@fn_library/empty_state' + + + + +``` + +## Notas + +El tamaño del icono escala con `size`: xs=32, sm=40, md=48, lg=64, xl=80. +El orden del heading (`Title order`) es 5 para xs/sm y 4 para md/lg/xl. +El botón usa `variant="light"` de Mantine — hereda el color primario del tema. +`children` se renderiza entre la descripción y el botón, útil para filtros o links adicionales. diff --git a/frontend/functions/ui/empty_state.tsx b/frontend/functions/ui/empty_state.tsx new file mode 100644 index 00000000..b57b176a --- /dev/null +++ b/frontend/functions/ui/empty_state.tsx @@ -0,0 +1,55 @@ +import * as React from 'react' +import { Button, Stack, Text, Title, type MantineSize } from '@mantine/core' +import { IconInbox } from '@tabler/icons-react' + +interface EmptyStateProps { + /** Icono a mostrar (default: IconInbox) */ + icon?: React.ReactNode + /** Título */ + title?: string + /** Descripción */ + description?: string + /** Texto del botón de acción */ + actionLabel?: string + /** Callback del botón */ + onAction?: () => void + /** Tamaño general */ + size?: MantineSize + /** Contenido custom debajo de la descripción */ + children?: React.ReactNode +} + +function EmptyState({ + icon, + title = 'No data found', + description = 'There are no items to display yet.', + actionLabel, + onAction, + size = 'md', + children, +}: EmptyStateProps) { + const iconSize = size === 'xs' ? 32 : size === 'sm' ? 40 : size === 'lg' ? 64 : size === 'xl' ? 80 : 48 + + return ( + + + {icon || } + + + {title} + + + {description} + + {children} + {actionLabel && onAction && ( + + )} + + ) +} + +export { EmptyState } +export type { EmptyStateProps } diff --git a/frontend/functions/ui/error_page.md b/frontend/functions/ui/error_page.md new file mode 100644 index 00000000..8ba4b70a --- /dev/null +++ b/frontend/functions/ui/error_page.md @@ -0,0 +1,75 @@ +--- +name: error_page +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "ErrorPage(config: ErrorPageConfig): JSX.Element" +description: "Genera página de error con código grande, título, descripción y acciones. Soporta 404, 500, 403 y cualquier código custom." +tags: [error, 404, 500, page, empty-state, ui, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [react, "@mantine/core"] +params: + - name: code + desc: "Código de error numérico o string a mostrar prominentemente (404, 500, 403, o cualquier valor custom)" + - name: title + desc: "Título del error mostrado bajo el código. Default: 'Page not found'" + - name: description + desc: "Descripción explicativa del error. Default: mensaje genérico de página no encontrada" + - name: actionLabel + desc: "Texto del botón de acción principal. Default: 'Go back to home'" + - name: onAction + desc: "Callback invocado al pulsar el botón de acción principal" + - name: extraActions + desc: "Nodos React adicionales renderizados junto al botón principal (botones secundarios, links, etc.)" +output: "Página de error centrada con código prominente, mensaje descriptivo y botones de acción" +has_state: false +framework: react +emits: [onAction] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/error_page.tsx" +--- + +## Ejemplo + +```tsx +import { ErrorPage } from '@fn_library/error_page' +import { Button } from '@mantine/core' + +// 404 con defaults + navigate('/')} /> + +// 500 custom + window.location.reload()} +/> + +// 403 con acciones extra + navigate('/dashboard')} + extraActions={ + + } +/> +``` + +## Notas + +El código de error se muestra con `fz={120}` y opacidad reducida (0.25) para crear un efecto visual de fondo sin distraer del mensaje. Acepta `number | string` para soportar códigos custom como "503" o "Maintenance". diff --git a/frontend/functions/ui/error_page.tsx b/frontend/functions/ui/error_page.tsx new file mode 100644 index 00000000..b93dbb4a --- /dev/null +++ b/frontend/functions/ui/error_page.tsx @@ -0,0 +1,56 @@ +import * as React from 'react' +import { Button, Container, Group, Stack, Text, Title } from '@mantine/core' + +interface ErrorPageConfig { + /** Código de error (404, 500, 403, etc.) */ + code?: number | string + /** Título del error */ + title?: string + /** Descripción del error */ + description?: string + /** Texto del botón de acción */ + actionLabel?: string + /** Callback del botón */ + onAction?: () => void + /** Acciones extra además del botón principal */ + extraActions?: React.ReactNode +} + +function ErrorPage({ + code = 404, + title = 'Page not found', + description = 'The page you are looking for does not exist. You may have mistyped the address, or the page has been moved to another URL.', + actionLabel = 'Go back to home', + onAction, + extraActions, +}: ErrorPageConfig) { + return ( + + + + {code} + + + {title} + + + {description} + + + + {extraActions} + + + + ) +} + +export { ErrorPage } +export type { ErrorPageConfig } diff --git a/frontend/functions/ui/file_input.md b/frontend/functions/ui/file_input.md new file mode 100644 index 00000000..1795a80c --- /dev/null +++ b/frontend/functions/ui/file_input.md @@ -0,0 +1,102 @@ +--- +name: file_input +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FileInput(props: FileInputProps): JSX.Element" +description: "Input de archivos con soporte para múltiples archivos, tipos aceptados y botón de limpiar. Wrapper sobre Mantine FileInput." +tags: [file, upload, input, form, component, ui, interactive, mantine] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/file_input.tsx" +framework: react +has_state: true +emits: [onChange] +props: + - name: multiple + type: "boolean" + required: false + description: "Permite seleccionar múltiples archivos" + - name: accept + type: "string" + required: false + description: "Tipos MIME o extensiones aceptadas (ej: 'image/*', '.pdf,.docx')" + - name: clearable + type: "boolean" + required: false + description: "Muestra botón para limpiar el archivo seleccionado" + - name: value + type: "File | File[] | null" + required: false + description: "Valor controlado del input" + - name: onChange + type: "(value: File | File[] | null) => void" + required: false + description: "Callback que se dispara al seleccionar o limpiar un archivo" + - name: placeholder + type: "string" + required: false + description: "Texto mostrado cuando no hay archivo seleccionado" + - name: label + type: "string" + required: false + description: "Etiqueta visible sobre el input" + - name: disabled + type: "boolean" + required: false + description: "Deshabilita el input" + - name: size + type: "'xs' | 'sm' | 'md' | 'lg' | 'xl'" + required: false + description: "Tamaño del componente" +params: + - name: props + desc: "Props de FileInput: archivo(s) seleccionado(s), tipos aceptados, modo múltiple, estado controlado y apariencia" +output: "Componente FileInput que renderiza input para selección de archivos con preview del nombre" +--- + +## Ejemplo + +```tsx +import { FileInput } from '@fn_library' + +// Archivo único + + +// Múltiples archivos + + +// Solo imágenes + +``` + +## Notas + +Wrapper directo sobre `FileInput` de `@mantine/core`. Acepta todas las props de Mantine sin restricciones. + +Para `multiple: true`, el tipo de `value` y `onChange` cambia a `File[] | null` automáticamente gracias al tipado genérico de Mantine. + +El prop `clearable` añade un ícono de X que permite vaciar la selección sin reabrir el explorador de archivos. diff --git a/frontend/functions/ui/file_input.tsx b/frontend/functions/ui/file_input.tsx new file mode 100644 index 00000000..38a551de --- /dev/null +++ b/frontend/functions/ui/file_input.tsx @@ -0,0 +1,10 @@ +import { FileInput as MantineFileInput, type FileInputProps as MantineFileInputProps } from '@mantine/core' + +interface FileInputProps extends MantineFileInputProps {} + +function FileInput(props: FileInputProps) { + return +} + +export { FileInput } +export type { FileInputProps } diff --git a/frontend/functions/ui/form_field.md b/frontend/functions/ui/form_field.md index 6913ced7..3c4670a9 100644 --- a/frontend/functions/ui/form_field.md +++ b/frontend/functions/ui/form_field.md @@ -8,12 +8,12 @@ purity: impure signature: "FormField(props: FormFieldProps): JSX.Element" description: "Wrapper de campo de formulario con label, helper text, error y ARIA automáticos. Inyecta id y aria-describedby a hijos." tags: [form, field, label, error, component, ui, accessibility] -uses_functions: [cn_ts_core] +uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" -imports: [react] +imports: ["@mantine/core"] output: "Componente FormField que renderiza wrapper de campo con label, helper text, error y ARIA automáticos" tested: false tests: [] diff --git a/frontend/functions/ui/form_field.tsx b/frontend/functions/ui/form_field.tsx index d238aa43..0504161b 100644 --- a/frontend/functions/ui/form_field.tsx +++ b/frontend/functions/ui/form_field.tsx @@ -1,5 +1,5 @@ -import * as React from "react" -import { cn } from "../core/cn" +import * as React from 'react' +import { Box, Text } from '@mantine/core' interface FormFieldProps { label?: string @@ -15,26 +15,39 @@ function FormField({ label, helperText, error, children, className }: FormFieldP const helperId = `${id}-helper` const errorId = `${id}-error` - const describedBy = [helperText ? helperId : null, error ? errorId : null].filter(Boolean).join(" ") || undefined + const describedBy = [helperText ? helperId : null, error ? errorId : null].filter(Boolean).join(' ') || undefined const childWithProps = React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child as React.ReactElement>, { id: inputId, - "aria-invalid": error ? true : undefined, - "aria-describedby": describedBy, + 'aria-invalid': error ? true : undefined, + 'aria-describedby': describedBy, + error: error || undefined, }) } return child }) return ( -
- {label && } + + {label && ( + + {label} + + )} {childWithProps} - {helperText && !error &&

{helperText}

} - {error &&

{error}

} -
+ {helperText && !error && ( + + {helperText} + + )} + {error && ( + + {error} + + )} + ) } diff --git a/frontend/functions/ui/graph/index.tsx b/frontend/functions/ui/graph/index.tsx index e1fb584a..baac506e 100644 --- a/frontend/functions/ui/graph/index.tsx +++ b/frontend/functions/ui/graph/index.tsx @@ -49,6 +49,12 @@ export interface GraphTheme { selectionColor?: string } +export interface ContextMenuTarget { + type: "node" | "edge" | "canvas" + id?: string + data?: GraphNode | GraphEdge +} + export interface GraphContainerProps { data: GraphData layout?: "organic" | "random" @@ -58,6 +64,7 @@ export interface GraphContainerProps { nodeTypes?: NodeType[] onNodeClick?: (node: GraphNode) => void onNodeDoubleClick?: (node: GraphNode) => void + onContextMenu?: (event: MouseEvent, target: ContextMenuTarget) => void enableSelection?: boolean selectionMode?: "single" | "multiple" theme?: GraphTheme @@ -84,6 +91,7 @@ function GraphContainer({ nodeTypes = [], onNodeClick, onNodeDoubleClick, + onContextMenu, theme: themeProp, height = "100%", className, @@ -96,10 +104,30 @@ function GraphContainer({ [themeProp], ) - // Build + render + // Build + render — wait for container to have dimensions + const [ready, setReady] = React.useState(false) React.useEffect(() => { const el = containerRef.current if (!el) return + if (el.clientHeight > 0 && el.clientWidth > 0) { + setReady(true) + return + } + const ro = new ResizeObserver((entries) => { + for (const entry of entries) { + if (entry.contentRect.height > 0 && entry.contentRect.width > 0) { + setReady(true) + ro.disconnect() + } + } + }) + ro.observe(el) + return () => ro.disconnect() + }, []) + + React.useEffect(() => { + const el = containerRef.current + if (!el || !ready) return // Cleanup previous instance if (sigmaRef.current) { @@ -110,7 +138,7 @@ function GraphContainer({ const g = new Graph({ multi: true, type: "directed" }) graphRef.current = g - // Add nodes + // Add nodes — store entity type as entityType to avoid sigma interpreting it as render program for (const n of data.nodes) { g.addNode(n.id, { label: n.label, @@ -118,7 +146,7 @@ function GraphContainer({ y: n.y ?? (Math.random() - 0.5) * 10, size: n.size ?? theme.nodeSize, color: n.color ?? theme.nodeColor, - type: n.type, + entityType: n.type, }) } @@ -152,6 +180,7 @@ function GraphContainer({ // Render const renderer = new Sigma(g, el, { + allowInvalidContainer: true, renderEdgeLabels: false, defaultEdgeColor: theme.edgeColor, defaultNodeColor: theme.nodeColor, @@ -174,13 +203,30 @@ function GraphContainer({ onNodeDoubleClick({ id: node, ...attrs } as unknown as GraphNode) }) } + if (onContextMenu) { + renderer.on("rightClickNode", ({ node, event }) => { + const mouseEvent = event.original as MouseEvent + mouseEvent.preventDefault() + const attrs = g.getNodeAttributes(node) + onContextMenu(mouseEvent, { + type: "node", + id: node, + data: { id: node, ...attrs } as unknown as GraphNode, + }) + }) + renderer.on("rightClickStage", ({ event }) => { + const mouseEvent = event.original as MouseEvent + mouseEvent.preventDefault() + onContextMenu(mouseEvent, { type: "canvas" }) + }) + } return () => { renderer.kill() sigmaRef.current = null graphRef.current = null } - }, [data, layout, theme, onNodeClick, onNodeDoubleClick]) + }, [data, layout, theme, onNodeClick, onNodeDoubleClick, onContextMenu, ready]) // Container background const containerStyle: React.CSSProperties = { diff --git a/frontend/functions/ui/index.ts b/frontend/functions/ui/index.ts index 9f674c01..906cf6c1 100644 --- a/frontend/functions/ui/index.ts +++ b/frontend/functions/ui/index.ts @@ -8,7 +8,8 @@ export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, Di export { Input, InputGroup, InputIcon } from './input' export { Label } from './label' export { KPICard } from './kpi_card' -export { Select, SelectContent, SelectGroup, SelectGroupLabel, SelectItem, SelectPortal, SelectSeparator, SelectTrigger, SelectValue } from './select' +export { Select } from './select' +export type { SelectProps } from './select' export { SimpleSelect } from './simple_select' export type { SimpleSelectOption, SimpleSelectGroup, SimpleSelectOptions } from './simple_select' export { Skeleton, SkeletonAvatar, SkeletonButton, SkeletonCard, SkeletonTable, SkeletonText } from './skeleton' @@ -37,11 +38,9 @@ export type { Series } from './chart_container' export { DataTable } from './data_table' export type { DataTableProps, ColumnDef } from './data_table' -// Theme -export { ThemeProvider, useTheme, ThemeContext } from './theme_provider' -export type { ThemeProviderProps } from './theme_provider' -export { applyTheme } from './apply_theme' -export type { Theme, ThemeColors } from './apply_theme' +// Mantine Provider +export { FnMantineProvider } from './mantine_provider' +export type { FnMantineProviderProps } from './mantine_provider' // Page templates export { analyticsPage } from './analytics_page' @@ -82,14 +81,14 @@ export type { CheckboxProps } from './checkbox' // Command export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSearch, CommandSeparator, CommandShortcut } from './command' -export type { CommandProps } from './command' +export type { CommandItemData, CommandProps } from './command' // Dropdown Menu export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from './dropdown_menu' // Pagination -export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from './pagination' -export type { PaginationLinkProps } from './pagination' +export { Pagination } from './pagination' +export type { PaginationProps } from './pagination' // Popover export { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverHeader, PopoverPortal, PopoverTitle, PopoverTrigger } from './popover' @@ -123,3 +122,15 @@ export { useAnimatedCanvas } from './use_animated_canvas' // Wails Provider export { WailsProvider } from './wails_provider' + +// New Mantine components +export { FnAppShell } from './app_shell' +export { FnStepper } from './stepper' +export { FnTimeline } from './timeline' +export { FnActionIcon } from './action_icon' +export { FnNumberInput } from './number_input' +export { FnSegmentedControl } from './segmented_control' +export { FnLoadingOverlay } from './loading_overlay' +export { FnRingProgress } from './ring_progress' +export { FnNavLink } from './nav_link' +export { FnIndicator } from './indicator' diff --git a/frontend/functions/ui/indicator.md b/frontend/functions/ui/indicator.md new file mode 100644 index 00000000..379f3be1 --- /dev/null +++ b/frontend/functions/ui/indicator.md @@ -0,0 +1,77 @@ +--- +name: indicator +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "FnIndicator(props: FnIndicatorProps): JSX.Element" +description: "Badge indicador posicionado sobre un elemento hijo. Wrapper sobre Mantine Indicator." +tags: [mantine, indicator, badge, notification, dot, component, ui] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["@mantine/core"] +framework: react +props: + - name: children + type: "ReactNode" + required: true + description: "Elemento sobre el cual se posiciona el indicador" + - name: color + type: "MantineColor" + required: false + description: "Color del indicador, default red" + - name: size + type: "number" + required: false + description: "Tamano del dot en px, default 10" + - name: position + type: "'top-start' | 'top-center' | 'top-end' | 'middle-start' | 'middle-center' | 'middle-end' | 'bottom-start' | 'bottom-center' | 'bottom-end'" + required: false + description: "Posicion del indicador, default top-end" + - name: processing + type: "boolean" + required: false + description: "Animacion de pulso, default false" + - name: disabled + type: "boolean" + required: false + description: "Oculta el indicador, default false" + - name: label + type: "ReactNode" + required: false + description: "Contenido dentro del indicador (numero, texto)" +output: "Elemento hijo con un dot/badge indicador posicionado en una esquina" +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/indicator.tsx" +emits: [] +has_state: false +variant: [] +--- + +## Ejemplo + +```tsx +import { FnIndicator } from '@fn_library' +import { FnActionIcon } from '@fn_library' +import { IconBell } from '@tabler/icons-react' + +{/* Dot simple */} + + } /> + + +{/* Con contador */} + + + +``` + +## Notas + +Wrapper sobre Mantine `Indicator`. El `processing` prop agrega una animacion de pulso al dot. Si se provee `label`, el indicador se agranda para mostrar contenido. `disabled` oculta el indicador sin desmontar el componente. diff --git a/frontend/functions/ui/indicator.tsx b/frontend/functions/ui/indicator.tsx new file mode 100644 index 00000000..13d130c0 --- /dev/null +++ b/frontend/functions/ui/indicator.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' +import { Indicator } from '@mantine/core' +import type { MantineColor } from '@mantine/core' + +interface FnIndicatorProps { + children: React.ReactNode + color?: MantineColor + size?: number + position?: 'top-start' | 'top-center' | 'top-end' | 'middle-start' | 'middle-center' | 'middle-end' | 'bottom-start' | 'bottom-center' | 'bottom-end' + processing?: boolean + disabled?: boolean + label?: React.ReactNode +} + +function FnIndicator({ + children, + color = 'red', + size = 10, + position = 'top-end', + processing = false, + disabled = false, + label, +}: FnIndicatorProps) { + return ( + + {children} + + ) +} + +export { FnIndicator } +export type { FnIndicatorProps } diff --git a/frontend/functions/ui/input.md b/frontend/functions/ui/input.md index c0f197ba..b0ad72fe 100644 --- a/frontend/functions/ui/input.md +++ b/frontend/functions/ui/input.md @@ -6,14 +6,14 @@ domain: ui version: "1.0.0" purity: impure signature: "Input(props: InputHTMLAttributes): JSX.Element" -description: "Campo de entrada accesible con soporte para iconos, grupos, validación ARIA y estados disabled/invalid." -tags: [input, form, component, ui, interactive] -uses_functions: [cn_ts_core] +description: "Campo de entrada accesible con soporte para iconos, grupos, validación ARIA y estados disabled/invalid. Mantine TextInput." +tags: [input, form, component, ui, interactive, mantine] +uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" -imports: ["@base-ui/react", "react"] +imports: ["@mantine/core"] output: "Componente Input que renderiza campo de entrada accesible con soporte para iconos y validación ARIA" tested: false tests: [] @@ -49,4 +49,4 @@ source_file: "frontend/src/components/ui/input.tsx" ## Notas -Exporta Input, InputGroup e InputIcon. InputGroup detecta automáticamente la presencia de iconos y ajusta padding del Input. +Exporta Input, InputGroup e InputIcon. Usa Mantine TextInput internamente. InputGroup e InputIcon se mantienen como wrappers de compatibilidad — para nuevos usos preferir leftSection/rightSection de Mantine TextInput directamente. diff --git a/frontend/functions/ui/input.tsx b/frontend/functions/ui/input.tsx index e5168797..1e45468f 100644 --- a/frontend/functions/ui/input.tsx +++ b/frontend/functions/ui/input.tsx @@ -1,18 +1,17 @@ -import * as React from "react" -import { Input as InputPrimitive } from "@base-ui/react/input" -import { cn } from "../core/cn" +import * as React from 'react' +import { TextInput, Box } from '@mantine/core' -function Input({ className, type, ...props }: React.ComponentProps<"input">) { +function Input({ + className, + type, + ...props +}: React.ComponentProps & { type?: string }) { return ( - ) @@ -25,32 +24,34 @@ interface InputGroupProps { function InputGroup({ children, className }: InputGroupProps) { return ( -
+ {children} -
+ ) } interface InputIconProps { children: React.ReactNode - position: "start" | "end" + position: 'start' | 'end' className?: string } function InputIcon({ children, position, className }: InputIconProps) { return ( - {children} - + ) } export { Input, InputGroup, InputIcon } +export type { InputGroupProps, InputIconProps } diff --git a/frontend/functions/ui/kpi_card.md b/frontend/functions/ui/kpi_card.md index 90699eed..ef94bf34 100644 --- a/frontend/functions/ui/kpi_card.md +++ b/frontend/functions/ui/kpi_card.md @@ -8,7 +8,7 @@ purity: impure signature: "KPICard(props: KPICardProps): JSX.Element" description: "Card de KPI con label, valor+unidad, delta descriptivo con color semántico, icono, slot de chart inline y action. 3 tamaños." tags: [kpi, card, metrics, dashboard, component, ui, sparkline] -uses_functions: [cn_ts_core] +uses_functions: [] uses_types: [] returns: [] returns_optional: false diff --git a/frontend/functions/ui/kpi_card.tsx b/frontend/functions/ui/kpi_card.tsx index 4d774c55..7c31d0b3 100644 --- a/frontend/functions/ui/kpi_card.tsx +++ b/frontend/functions/ui/kpi_card.tsx @@ -1,78 +1,87 @@ import * as React from 'react' -import { cn } from '../core/cn' +import { Paper, Text, Group, Stack, Box } from '@mantine/core' type KPICardSize = 'sm' | 'default' | 'lg' interface Delta { value: number isPositive: boolean - /** Descriptive label before value, e.g. "Increased by" */ label?: string - /** Suffix after value, e.g. "vs yesterday" */ suffix?: string } interface KPICardProps extends React.HTMLAttributes { label: string value: string | number - /** Unit displayed next to value in smaller font, e.g. "k", "ms", "%" */ unit?: string delta?: Delta icon?: React.ReactNode - /** Action slot rendered top-right, e.g. a menu button */ action?: React.ReactNode - /** Inline chart slot rendered to the right of the value */ chart?: React.ReactNode subtitle?: string size?: KPICardSize } -const sizeStyles: Record = { - sm: { value: 'text-2xl font-bold', unit: 'text-base font-medium', label: 'text-xs' }, - default: { value: 'text-3xl font-bold', unit: 'text-lg font-medium', label: 'text-sm' }, - lg: { value: 'text-4xl font-bold', unit: 'text-xl font-medium', label: 'text-base' }, +const valueSizes: Record = { + sm: '1.5rem', + default: '1.875rem', + lg: '2.25rem', +} + +const unitSizes: Record = { + sm: 'md', + default: 'lg', + lg: 'xl', +} + +const labelSizes: Record = { + sm: 'xs', + default: 'sm', + lg: 'md', } const KPICard = React.forwardRef( ({ label, value, unit, delta, icon, action, chart, subtitle, size = 'default', className, ...props }, ref) => { - const styles = sizeStyles[size] const deltaColor = delta - ? delta.value === 0 ? 'text-muted-foreground' - : delta.isPositive ? 'text-green-600 dark:text-green-500' - : 'text-red-600 dark:text-red-500' - : '' + ? delta.value === 0 ? 'dimmed' + : delta.isPositive ? 'teal' + : 'red' + : undefined return ( -
-
-
- {icon &&
{icon}
} -
-

{label}

- {subtitle &&

{subtitle}

} -
-
- {action &&
{action}
} -
-
-
-
- {value} - {unit && {unit}} -
+ + + + {icon && {icon}} + + {label} + {subtitle && {subtitle}} + + + {action && {action}} + + + + + + + {value} + + {unit && {unit}} + {delta && ( -
- {delta.label && {delta.label}} - - {delta.isPositive ? '▲' : '▼'} {delta.value > 0 ? '+' : ''}{delta.value}{delta.label ? '' : '%'} - - {delta.suffix && {delta.suffix}} -
+ + {delta.label && {delta.label}} + + {delta.isPositive ? '\u25B2' : '\u25BC'} {delta.value > 0 ? '+' : ''}{delta.value}{delta.label ? '' : '%'} + + {delta.suffix && {delta.suffix}} + )} -
- {chart &&
{chart}
} -
-
+ + {chart && {chart}} + + ) } ) diff --git a/frontend/functions/ui/label.md b/frontend/functions/ui/label.md index 7dd73b2b..08cad4bf 100644 --- a/frontend/functions/ui/label.md +++ b/frontend/functions/ui/label.md @@ -6,14 +6,14 @@ domain: ui version: "1.0.0" purity: impure signature: "Label(props: LabelHTMLAttributes): JSX.Element" -description: "Etiqueta de formulario accesible con soporte para estados disabled y peer-disabled." -tags: [label, form, component, ui] -uses_functions: [cn_ts_core] +description: "Etiqueta de formulario accesible con soporte para estados disabled. Mantine Text con component=label." +tags: [label, form, component, ui, mantine] +uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" -imports: ["react"] +imports: ["@mantine/core"] output: "Componente Label que renderiza etiqueta de formulario accesible con soporte para estados disabled" tested: false tests: [] diff --git a/frontend/functions/ui/label.tsx b/frontend/functions/ui/label.tsx index 56bb58a6..0ab0f331 100644 --- a/frontend/functions/ui/label.tsx +++ b/frontend/functions/ui/label.tsx @@ -1,14 +1,15 @@ -import * as React from "react" -import { cn } from "../core/cn" +import * as React from 'react' +import { Text } from '@mantine/core' -function Label({ className, ...props }: React.ComponentProps<"label">) { +function Label({ className, ...props }: React.ComponentProps<'label'>) { return ( -