refactor(frontend): migracion a Mantine y limpieza de widgets

- Migra el frontend a Mantine v9 siguiendo la regla de theming del registry (@fn_library, sin Tailwind/cn/CVA).
- Reescribe DashboardShell, FilterBar, Section, WidgetRenderer y todos los widgets (Area/Bar/Line/Pie/KPI/Sparkline/Table) con componentes y props de Mantine.
- Ajusta vite.config, main.tsx, App.tsx, app.css y env.d.ts.
- Añade postcss.config.cjs requerido por Mantine.
- Actualiza package.json y pnpm-lock.
- Ajusta config.go, main.go y los ejemplos (fn_registry_apps/overview) para el nuevo esquema de tipos en frontend/src/types.ts.
This commit is contained in:
2026-04-13 23:33:04 +02:00
parent b7f354e081
commit 4ec62f5ed6
25 changed files with 807 additions and 1096 deletions
+27 -21
View File
@@ -1,5 +1,6 @@
import { useState } from 'react'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { Box, UnstyledButton, Group, Text } from '@mantine/core'
import { IconChevronDown, IconChevronRight } from '@tabler/icons-react'
import { WidgetRenderer } from './WidgetRenderer'
import type { SectionDef, QueryDef } from '../types'
import type { FilterValues } from '../hooks/useFilterState'
@@ -10,42 +11,47 @@ interface SectionProps {
filters: FilterValues
globalColumns: number
getData: (widgetID: string, filters: FilterValues) => Promise<Record<string, unknown>[]>
fillHeight?: boolean
}
export function Section({ section, queries, filters, globalColumns, getData }: SectionProps) {
export function Section({ section, queries, filters, globalColumns, getData, fillHeight }: SectionProps) {
const [collapsed, setCollapsed] = useState(false)
const columns = section.columns || globalColumns
return (
<div className="space-y-2">
<button
className="flex items-center gap-2 cursor-pointer select-none bg-transparent border-none p-0"
<Box style={fillHeight ? { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: 'var(--mantine-spacing-xs)' } : { display: 'flex', flexDirection: 'column', gap: 'var(--mantine-spacing-xs)' }}>
<UnstyledButton
onClick={() => section.collapsible && setCollapsed(c => !c)}
type="button"
style={{ flexShrink: 0, cursor: section.collapsible ? 'pointer' : 'default', userSelect: 'none' }}
>
{section.collapsible && (
collapsed
? <ChevronRight className="w-4 h-4 text-[var(--muted-foreground)]" />
: <ChevronDown className="w-4 h-4 text-[var(--muted-foreground)]" />
)}
<h2 className="text-sm font-medium text-[var(--muted-foreground)] uppercase tracking-wider">
{section.title}
</h2>
</button>
<Group gap="xs">
{section.collapsible && (
collapsed
? <IconChevronRight size={16} style={{ color: 'var(--mantine-color-dimmed)' }} />
: <IconChevronDown size={16} style={{ color: 'var(--mantine-color-dimmed)' }} />
)}
<Text size="xs" fw={500} c="dimmed" tt="uppercase" style={{ letterSpacing: '0.05em' }}>
{section.title}
</Text>
</Group>
</UnstyledButton>
{!collapsed && (
<div
className="grid gap-2"
<Box
style={{
display: 'grid',
gap: 'var(--mantine-spacing-xs)',
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
...(fillHeight ? { flex: 1, minHeight: 0 } : {}),
}}
>
{section.widgets.map(widget => (
<div
<Box
key={widget.id}
style={{
gridColumn: `span ${Math.min(widget.span || 1, columns)}`,
gridRow: widget.rowSpan ? `span ${widget.rowSpan}` : undefined,
...(fillHeight ? { minHeight: 0, overflow: 'hidden' } : {}),
}}
>
<WidgetRenderer
@@ -54,10 +60,10 @@ export function Section({ section, queries, filters, globalColumns, getData }: S
filters={filters}
getData={getData}
/>
</div>
</Box>
))}
</div>
</Box>
)}
</div>
</Box>
)
}