sync: 2 new components + 2 improvements from fn_registry

- funnel_chart (new) — conversion funnel with gradient bars
- heatmap_grid (new) — generic rows × cols intensity matrix
- alert (1.1.0) — success/warning/info variants added
- data_table (1.1.0) — density prop (compact/cozy/roomy) added

Source: claude.ai/design export — Ads Analytics Dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Egutierrez
2026-04-21 21:20:45 +02:00
parent 344bdbd5e6
commit c38310ae60
9 changed files with 524 additions and 15 deletions
+6 -6
View File
@@ -3,10 +3,10 @@ name: alert
kind: component
lang: ts
domain: ui
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "Alert(props: { variant?: 'default' | 'destructive' }): JSX.Element"
description: "Alerta accesible con variantes default y destructive. Mantine Alert con slots para título, descripción y acción."
signature: "Alert(props: { variant?: 'default' | 'destructive' | 'success' | 'warning' | 'info' }): JSX.Element"
description: "Alerta accesible con 5 variantes semánticas (default, destructive, success, warning, info). Mantine Alert con slots para título, descripción y acción."
tags: [alert, feedback, component, ui, notification, mantine]
uses_functions: []
uses_types: []
@@ -21,13 +21,13 @@ test_file_path: ""
file_path: "frontend/functions/ui/alert.tsx"
props:
- name: variant
type: "'default' | 'destructive'"
type: "'default' | 'destructive' | 'success' | 'warning' | 'info'"
required: false
description: "Variante visual"
description: "Variante visual semántica."
emits: []
has_state: false
framework: react
variant: [default, destructive]
variant: [default, destructive, success, warning, info]
source_repo: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/Bl4cksmith/Frontend_Library"
source_license: "MIT"
source_file: "frontend/src/components/ui/alert.tsx"
+4 -1
View File
@@ -1,11 +1,14 @@
import * as React from 'react'
import { Alert as MantineAlert, Box, Text } from '@mantine/core'
type AlertVariant = 'default' | 'destructive'
type AlertVariant = 'default' | 'destructive' | 'success' | 'warning' | 'info'
const variantColorMap: Record<AlertVariant, string | undefined> = {
default: undefined,
destructive: 'red',
success: 'green',
warning: 'yellow',
info: 'blue',
}
function Alert({
+6 -2
View File
@@ -3,10 +3,10 @@ name: data_table
kind: component
lang: ts
domain: ui
version: "1.0.0"
version: "1.1.0"
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."
description: "Tabla de datos con sticky header, overflow scroll, heatmap por columna, formato condicional (number/datetime/currency), hover rows y densidad configurable (compact/cozy/roomy). Auto-detecta columnas desde la primera fila si no se proveen."
tags: [table, data, heatmap, dashboard, component, ui, format, visualization]
uses_functions: []
uses_types: []
@@ -44,6 +44,10 @@ props:
type: "Error | null"
required: false
description: "Error a mostrar si la carga falló."
- name: density
type: "'compact' | 'cozy' | 'roomy'"
required: false
description: "Padding vertical y horizontal de filas. compact=4/xs, cozy=6/sm (default), roomy=10/md."
emits: []
has_state: false
framework: react
+17 -5
View File
@@ -10,6 +10,8 @@ interface ColumnDef {
align?: 'left' | 'right' | 'center'
}
type DataTableDensity = 'compact' | 'cozy' | 'roomy'
interface DataTableProps {
data: Record<string, unknown>[]
columns?: ColumnDef[]
@@ -18,6 +20,14 @@ interface DataTableProps {
maxHeight?: number | string
loading?: boolean
error?: Error | null
/** Row padding preset. compact=4/8, cozy=6/12 (default), roomy=10/16. */
density?: DataTableDensity
}
const DENSITY_MAP: Record<DataTableDensity, { py: number; px: 'xs' | 'sm' | 'md' }> = {
compact: { py: 4, px: 'xs' },
cozy: { py: 6, px: 'sm' },
roomy: { py: 10, px: 'md' },
}
function formatCell(value: unknown, format?: string): string {
@@ -52,7 +62,9 @@ function DataTableComponent({
maxHeight = 500,
loading = false,
error = null,
density = 'cozy',
}: DataTableProps) {
const pad = DENSITY_MAP[density]
// Auto-detect columns from first row if not provided
const effectiveColumns: ColumnDef[] = (columns && columns.length > 0)
? columns
@@ -113,8 +125,8 @@ function DataTableComponent({
fw={500}
c="dimmed"
tt="uppercase"
py={6}
px="sm"
py={pad.py}
px={pad.px}
>
{col.label}
</Table.Th>
@@ -131,8 +143,8 @@ function DataTableComponent({
key={col.key}
style={{ textAlign: align, fontFamily: 'var(--mantine-font-family-monospace)', ...heatmapStyle(col.key, row[col.key]) }}
fz="xs"
py={6}
px="sm"
py={pad.py}
px={pad.px}
>
{formatCell(row[col.key], col.format)}
</Table.Td>
@@ -152,4 +164,4 @@ function DataTableComponent({
}
export const DataTable = DataTableComponent
export type { DataTableProps, ColumnDef }
export type { DataTableProps, ColumnDef, DataTableDensity }
+101
View File
@@ -0,0 +1,101 @@
---
name: funnel_chart
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "FunnelChart(props: FunnelChartProps): JSX.Element"
description: "Visualiza un funnel de conversión con barras degradadas, valores formateados y tasa de conversión entre etapas como Badge semántico. Genérico — acepta cualquier array {stage,value}."
tags: [funnel, conversion, dashboard, component, ui, chart, acquisition, analytics]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@mantine/core", react]
output: "Componente FunnelChart que renderiza un funnel vertical de conversión con porcentajes entre etapas."
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/funnel_chart.tsx"
props:
- name: data
type: "FunnelStage[]"
required: true
description: "Etapas del funnel. Cada etapa con { stage: string, value: number }."
- name: valueFormatter
type: "(value: number) => string"
required: false
description: "Formateador del valor absoluto. Default: compact (K/M/B)."
- name: showConversion
type: "boolean"
required: false
description: "Mostrar tasa de conversión entre etapas como Badge. Default true."
- name: barHeight
type: "number"
required: false
description: "Alto de cada barra en px. Default 28."
- name: goodThreshold
type: "number"
required: false
description: "Tasa de conversión (%) por encima de la cual el Badge es 'success'. Default 30."
- name: warnThreshold
type: "number"
required: false
description: "Tasa de conversión (%) por encima de la cual el Badge es 'info' (por debajo → 'warning'). Default 5."
- name: barLabel
type: "(stage: FunnelStage, pctOfMax: number) => string"
required: false
description: "Texto opcional dentro de cada barra."
- name: barColor
type: "string"
required: false
description: "Color o gradient CSS a aplicar a las barras. Default: gradient indigo→cyan."
emits: []
has_state: false
framework: react
variant: [default]
source_repo: "claude.ai/design"
source_license: ""
source_file: "sources/frontend_designs/Ads Analytics Dashboard _standalone_.html"
---
## Ejemplo
```tsx
import { FunnelChart } from '@fn_library'
const funnel = [
{ stage: 'Impressions', value: 12_400_000 },
{ stage: 'Clicks', value: 214_807 },
{ stage: 'Sessions', value: 186_904 },
{ stage: 'Add to cart', value: 24_113 },
{ stage: 'Checkout', value: 9_642 },
{ stage: 'Conversions', value: 4_812 },
]
// Básico — conversión automática entre etapas
<FunnelChart data={funnel} />
// Moneda + altura mayor
<FunnelChart
data={funnel}
valueFormatter={(n) => '$' + n.toLocaleString()}
barHeight={36}
/>
// Umbrales custom de conversión (ej. funnel de lead gen donde >10% ya es bueno)
<FunnelChart
data={leadFunnel}
goodThreshold={10}
warnThreshold={2}
/>
```
## Notas
- La barra se dimensiona como porcentaje del valor máximo (primera etapa por defecto).
- La tasa de conversión se calcula contra la **etapa anterior**, no contra el máximo.
- Extraído de un export de Claude Design (Ads Analytics Dashboard). El `AdsFunnel` original estaba hardcodeado con gradient indigo→cyan; aquí se generaliza con `barColor` y `barLabel`.
- Los tres colores semánticos del Badge (`success`/`info`/`warning`) son configurables vía `goodThreshold` y `warnThreshold`.
+106
View File
@@ -0,0 +1,106 @@
import * as React from 'react'
import { Stack, Group, Text, Box } from '@mantine/core'
import { Badge } from './badge'
interface FunnelStage {
stage: string
value: number
}
interface FunnelChartProps {
data: FunnelStage[]
/** Format function for the absolute value shown next to each stage. */
valueFormatter?: (value: number) => string
/** Show the inter-stage conversion rate as a badge. Default true. */
showConversion?: boolean
/** Height of each bar. Default 28. */
barHeight?: number
/** Conversion rate (%) above which the badge is `success`. Default 30. */
goodThreshold?: number
/** Conversion rate (%) above which the badge is `info` (below → `warning`). Default 5. */
warnThreshold?: number
/** Label shown inside each bar. `(stage, pctOfMax) => string`. Default empty. */
barLabel?: (stage: FunnelStage, pctOfMax: number) => string
/** Override the default gradient. Pass a single color or CSS gradient string. */
barColor?: string
className?: string
}
function defaultFormatter(n: number): string {
const abs = Math.abs(n)
if (abs >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, '') + 'B'
if (abs >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'
if (abs >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + 'K'
return String(Math.round(n))
}
const DEFAULT_BAR_GRADIENT =
'linear-gradient(90deg, var(--mantine-primary-color-filled), color-mix(in oklab, var(--mantine-color-cyan-4) 70%, var(--mantine-primary-color-filled)))'
function FunnelChart({
data,
valueFormatter = defaultFormatter,
showConversion = true,
barHeight = 28,
goodThreshold = 30,
warnThreshold = 5,
barLabel,
barColor,
className,
}: FunnelChartProps) {
if (!data || data.length === 0) return null
const max = Math.max(...data.map(d => d.value))
return (
<Stack gap="xs" className={className}>
{data.map((s, i) => {
const pct = max === 0 ? 0 : s.value / max
const convRate = i > 0 ? (s.value / (data[i - 1]!.value || 1)) * 100 : 100
const conversionVariant =
convRate > goodThreshold ? 'success' : convRate > warnThreshold ? 'info' : 'warning'
const label = barLabel?.(s, pct)
return (
<div key={s.stage}>
<Group justify="space-between" mb={4}>
<Text size="xs" c="dimmed">
{s.stage}
</Text>
<Group gap={8}>
<Text size="xs" fw={600}>
{valueFormatter(s.value)}
</Text>
{showConversion && i > 0 && (
<Badge size="sm" variant={conversionVariant}>
{convRate.toFixed(1)}%
</Badge>
)}
</Group>
</Group>
<Box
style={{
height: barHeight,
width: `${pct * 100}%`,
background: barColor ?? DEFAULT_BAR_GRADIENT,
borderRadius: 4,
display: 'flex',
alignItems: 'center',
paddingLeft: 10,
fontSize: 11,
fontWeight: 600,
color: 'white',
transition: 'width 400ms ease',
}}
>
{label}
</Box>
</div>
)
})}
</Stack>
)
}
export { FunnelChart }
export type { FunnelChartProps, FunnelStage }
+126
View File
@@ -0,0 +1,126 @@
---
name: heatmap_grid
kind: component
lang: ts
domain: ui
version: "1.0.0"
purity: impure
signature: "HeatmapGrid(props: HeatmapGridProps): JSX.Element"
description: "Matriz rows × columns con intensidad de color proporcional al valor. Genérico — casos típicos: day×hour, cohort retention, matriz de correlación, heatmap geográfico."
tags: [heatmap, matrix, dashboard, component, ui, chart, retention, cohort]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["@mantine/core", react]
output: "Componente HeatmapGrid que renderiza una tabla de celdas coloreadas por intensidad con labels formateados."
tested: false
tests: []
test_file_path: ""
file_path: "frontend/functions/ui/heatmap_grid.tsx"
props:
- name: rows
type: "Record<string, unknown>[]"
required: true
description: "Filas de datos. Cada objeto contiene el label de fila (bajo rowKey) y los valores (bajo las keys de columns)."
- name: rowKey
type: "string"
required: true
description: "Key en cada row donde está el label de la fila (ej. 'day', 'cohort')."
- name: columns
type: "HeatmapColumn[]"
required: true
description: "Columnas a visualizar, en orden. Cada una: { key, label? }."
- name: valueFormatter
type: "(v: number) => string"
required: false
description: "Formateador del valor numérico. Default: int o 2 decimales."
- name: rowLabelFormatter
type: "(row: Record<string, unknown>) => string"
required: false
description: "Formatear el label de fila. Default: String(row[rowKey])."
- name: tooltip
type: "(row, column, value) => string"
required: false
description: "Generador del tooltip (title HTML nativo)."
- name: cellLabelThreshold
type: "number"
required: false
description: "Mostrar el valor dentro de la celda solo si |v| ≥ threshold. Default: siempre mostrar."
- name: cellSize
type: "number"
required: false
description: "Tamaño de cada celda en px. Default 22."
- name: colLabelEvery
type: "number"
required: false
description: "Mostrar label de columna cada N columnas (grids densos). Default 1."
- name: intensityRange
type: "[number, number]"
required: false
description: "Min/max % del baseColor aplicado vía color-mix. Default [6, 84]."
- name: baseColor
type: "string"
required: false
description: "Color base. Default 'var(--mantine-primary-color-filled)'."
emits: []
has_state: false
framework: react
variant: [default]
source_repo: "claude.ai/design"
source_license: ""
source_file: "sources/frontend_designs/Ads Analytics Dashboard _standalone_.html"
---
## Ejemplo
```tsx
import { HeatmapGrid } from '@fn_library'
// CTR por día × hora (caso del export original)
const hours = Array.from({ length: 24 }, (_, i) => ({ key: 'h' + i, label: String(i).padStart(2, '0') }))
const rows = [
{ day: 'Mon', h0: 0.8, h1: 0.7, /* ... */ h23: 1.4 },
{ day: 'Tue', h0: 0.9, h1: 0.8, /* ... */ h23: 1.3 },
// ...
]
<HeatmapGrid
rows={rows}
rowKey="day"
columns={hours}
colLabelEvery={2}
valueFormatter={(v) => v.toFixed(2)}
cellLabelThreshold={1.7}
tooltip={(r, c, v) => `${r.day} ${c.label}:00 — CTR ${v}%`}
/>
// Cohort retention (semana 0-12)
const cohortCols = Array.from({ length: 13 }, (_, i) => ({ key: 'w' + i, label: 'W' + i }))
<HeatmapGrid
rows={cohorts}
rowKey="cohort"
columns={cohortCols}
valueFormatter={(v) => v.toFixed(0) + '%'}
intensityRange={[10, 90]}
/>
// Matriz de correlación
<HeatmapGrid
rows={correlationMatrix}
rowKey="variable"
columns={variables.map(v => ({ key: v, label: v }))}
valueFormatter={(v) => v.toFixed(2)}
cellSize={40}
baseColor="var(--mantine-color-cyan-6)"
/>
```
## Notas
- El color de cada celda es `color-mix(in oklab, baseColor N%, transparent)` donde N interpola linealmente entre `intensityRange[0]` y `intensityRange[1]` según el valor normalizado en `[min, max]`.
- El color del texto en la celda cambia automáticamente a blanco si `v > mid` (mid-point del rango) para contraste.
- Para grids densos (ej. 24 horas × 7 días) usar `colLabelEvery={2}` o mayor para que los headers no se solapen.
- Extraído de un export de Claude Design (Ads Analytics Dashboard). El `Heatmap` original hardcodeaba `h0..h23`; aquí se generaliza aceptando cualquier array de columnas.
+153
View File
@@ -0,0 +1,153 @@
import * as React from 'react'
import { Box } from '@mantine/core'
interface HeatmapColumn {
key: string
label?: string
}
interface HeatmapGridProps {
/** Row data. Each row is an object that contains values under the column keys and the row label under `rowKey`. */
rows: Record<string, unknown>[]
/** Key in each row that holds the row label (e.g. 'day', 'cohort'). */
rowKey: string
/** Columns to visualize, in order. Each entry has the value key and an optional display label. */
columns: HeatmapColumn[]
/** Formatter for numeric values inside cells and tooltip. */
valueFormatter?: (v: number) => string
/** Formatter for row labels (left column). Defaults to String(row[rowKey]). */
rowLabelFormatter?: (row: Record<string, unknown>) => string
/** Tooltip text generator (native HTML title). */
tooltip?: (row: Record<string, unknown>, column: HeatmapColumn, value: number) => string
/** Render the numeric label inside the cell only if |value| ≥ threshold. Default: always render. */
cellLabelThreshold?: number
/** Cell size in px. Default 22. */
cellSize?: number
/** Render a column header label every N columns (keeps the grid compact for hourly grids). Default 1. */
colLabelEvery?: number
/** Min/Max percent of primary color applied via color-mix. Default [6, 84]. */
intensityRange?: [number, number]
/** Override the base color. Default is `var(--mantine-primary-color-filled)`. */
baseColor?: string
className?: string
}
function defaultValueFormatter(v: number): string {
return Number.isInteger(v) ? String(v) : v.toFixed(2)
}
function HeatmapGrid({
rows,
rowKey,
columns,
valueFormatter = defaultValueFormatter,
rowLabelFormatter,
tooltip,
cellLabelThreshold,
cellSize = 22,
colLabelEvery = 1,
intensityRange = [6, 84],
baseColor = 'var(--mantine-primary-color-filled)',
className,
}: HeatmapGridProps) {
const { min, max, mid } = React.useMemo(() => {
const vals: number[] = []
for (const row of rows) {
for (const col of columns) {
const n = Number(row[col.key])
if (!isNaN(n)) vals.push(n)
}
}
if (vals.length === 0) return { min: 0, max: 0, mid: 0 }
const mn = Math.min(...vals)
const mx = Math.max(...vals)
return { min: mn, max: mx, mid: (mn + mx) / 2 }
}, [rows, columns])
const [lo, hi] = intensityRange
function cellBg(v: number): string {
if (max === min) return `color-mix(in oklab, ${baseColor} ${lo}%, transparent)`
const t = (v - min) / (max - min)
const pct = lo + t * (hi - lo)
return `color-mix(in oklab, ${baseColor} ${pct.toFixed(0)}%, transparent)`
}
return (
<Box style={{ overflow: 'auto' }} className={className}>
<table style={{ borderCollapse: 'separate', borderSpacing: 2, width: '100%' }}>
<thead>
<tr>
<th />
{columns.map((col, i) => {
if (colLabelEvery > 1 && i % colLabelEvery !== 0) return <th key={col.key} />
const span = Math.min(colLabelEvery, columns.length - i)
return (
<th
key={col.key}
colSpan={span}
style={{
fontSize: 10,
color: 'var(--mantine-color-dimmed)',
fontWeight: 500,
textAlign: 'left',
}}
>
{col.label ?? col.key}
</th>
)
})}
</tr>
</thead>
<tbody>
{rows.map((row, rowIdx) => {
const label = rowLabelFormatter ? rowLabelFormatter(row) : String(row[rowKey] ?? '')
return (
<tr key={String(row[rowKey] ?? rowIdx)}>
<td
style={{
fontSize: 11,
color: 'var(--mantine-color-dimmed)',
paddingRight: 8,
whiteSpace: 'nowrap',
}}
>
{label}
</td>
{columns.map(col => {
const raw = Number(row[col.key])
const v = isNaN(raw) ? 0 : raw
const showLabel =
cellLabelThreshold == null ? true : Math.abs(v) >= cellLabelThreshold
const title = tooltip
? tooltip(row, col, v)
: `${label} · ${col.label ?? col.key}${valueFormatter(v)}`
return (
<td
key={col.key}
title={title}
style={{
width: cellSize,
height: cellSize,
background: cellBg(v),
borderRadius: 3,
fontSize: 9,
textAlign: 'center',
color: v > mid ? 'white' : 'var(--mantine-color-dimmed)',
}}
>
{showLabel ? valueFormatter(v) : ''}
</td>
)
})}
</tr>
)
})}
</tbody>
</table>
</Box>
)
}
export { HeatmapGrid }
export type { HeatmapGridProps, HeatmapColumn }
+5 -1
View File
@@ -36,7 +36,11 @@ export type { Series } from './chart_container'
// Data
export { DataTable } from './data_table'
export type { DataTableProps, ColumnDef } from './data_table'
export type { DataTableProps, ColumnDef, DataTableDensity } from './data_table'
export { FunnelChart } from './funnel_chart'
export type { FunnelChartProps, FunnelStage } from './funnel_chart'
export { HeatmapGrid } from './heatmap_grid'
export type { HeatmapGridProps, HeatmapColumn } from './heatmap_grid'
// Mantine Provider
export { FnMantineProvider } from './mantine_provider'