@asteby/metacore-runtime-react 18.17.0 → 18.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.17.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 211484b: Dashboard grid: compact KPI stat cards, dense fixed-row packing (charts span 2 rows), charts fill card height
8
+
3
9
  ## 18.17.0
4
10
 
5
11
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAW1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBA4IpB"}
1
+ {"version":3,"file":"dashboard-grid.d.ts","sourceRoot":"","sources":["../src/dashboard-grid.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,OAAO,KAAK,EACR,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAW1B,uEAAuE;AACvE,wBAAgB,eAAe,CAC3B,MAAM,CAAC,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,GAChC,oBAAoB,EAAE,CAgBxB;AASD,wBAAgB,aAAa,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,OAAO,GACV,EAAE,kBAAkB,qBA2IpB"}
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
9
9
  import { cn } from '@asteby/metacore-ui/lib';
10
10
  import { useCan, usePermissionsActive } from './permissions-context';
11
11
  import { DynamicIcon } from './dynamic-icon';
12
- import { WidgetRenderer, WidgetSkeleton, SIZE_CLASS } from './widgets/widget-renderer';
12
+ import { WidgetRenderer, WidgetSkeleton, spanClass } from './widgets/widget-renderer';
13
13
  const DEFAULT_STRINGS = {
14
14
  emptyTitle: 'Your dashboard is empty',
15
15
  emptyDescription: 'Install an addon with dashboard widgets to start seeing metrics here.',
@@ -110,9 +110,8 @@ export function DashboardGrid({ groups, widgets, loadData, isAdmin, locale, curr
110
110
  if (visibleGroups.length === 0) {
111
111
  return (_jsxs("div", { "data-testid": "dashboard-empty", className: cn('flex min-h-[40vh] flex-col items-center justify-center rounded-xl border border-dashed border-border/60 p-10 text-center', className), children: [_jsx("div", { className: "mb-4 flex size-14 items-center justify-center rounded-2xl bg-muted text-muted-foreground", children: _jsx(DynamicIcon, { name: "LayoutDashboard", className: "size-7" }) }), _jsx("h3", { className: "text-base font-semibold text-foreground", children: tr(undefined, s.emptyTitle) || s.emptyTitle }), _jsx("p", { className: "mt-1 max-w-sm text-sm text-muted-foreground", children: s.emptyDescription })] }));
112
112
  }
113
- return (_jsx("div", { "data-testid": "dashboard-grid", className: cn('flex flex-col gap-8', className), children: visibleGroups.map((group) => (_jsxs("section", { className: "flex flex-col gap-3", children: [group.title && (_jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: tr(group.title, group.title) })), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: group.widgets.map((spec) => {
114
- const size = spec.size ?? 'sm';
115
- return (_jsx("div", { className: SIZE_CLASS[size], children: loading ? (_jsx(WidgetSkeleton, { spec: {
113
+ return (_jsx("div", { "data-testid": "dashboard-grid", className: cn('flex flex-col gap-8', className), children: visibleGroups.map((group) => (_jsxs("section", { className: "flex flex-col gap-3", children: [group.title && (_jsx("h2", { className: "text-sm font-semibold uppercase tracking-wide text-muted-foreground", children: tr(group.title, group.title) })), _jsx("div", { className: "grid grid-flow-row-dense auto-rows-[116px] grid-cols-2 gap-4 lg:grid-cols-4", children: group.widgets.map((spec) => {
114
+ return (_jsx("div", { className: cn(spanClass(spec), 'min-h-0'), children: loading ? (_jsx(WidgetSkeleton, { spec: {
116
115
  ...spec,
117
116
  title: tr(spec.title, spec.title),
118
117
  subtitle: tr(spec.subtitle, spec.subtitle),
@@ -1 +1 @@
1
- {"version":3,"file":"renderers.d.ts","sourceRoot":"","sources":["../../src/widgets/renderers.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAkB9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAazE,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,mBAAmB,CAAA;IACzB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;CACpB;AAiCD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBA2B9C;AAGD,wBAAgB,SAAS,CAAC,CAAC,EAAE,iBAAiB,qBAsC7C;AAgDD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBAE9C;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBAE9C;AAqDD,wBAAgB,SAAS,CAAC,CAAC,EAAE,iBAAiB,qBAE7C;AACD,wBAAgB,WAAW,CAAC,CAAC,EAAE,iBAAiB,qBAE/C;AAGD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBAqC9C;AAKD,wBAAgB,cAAc,CAAC,CAAC,EAAE,iBAAiB,qBAkClD"}
1
+ {"version":3,"file":"renderers.d.ts","sourceRoot":"","sources":["../../src/widgets/renderers.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAkB9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAazE,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,mBAAmB,CAAA;IACzB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;CACpB;AA4CD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBA6B9C;AAGD,wBAAgB,SAAS,CAAC,CAAC,EAAE,iBAAiB,qBAsC7C;AAgDD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBAE9C;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBAE9C;AAuDD,wBAAgB,SAAS,CAAC,CAAC,EAAE,iBAAiB,qBAE7C;AACD,wBAAgB,WAAW,CAAC,CAAC,EAAE,iBAAiB,qBAE/C;AAGD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBAqC9C;AAKD,wBAAgB,cAAc,CAAC,CAAC,EAAE,iBAAiB,qBAkClD"}
@@ -5,7 +5,12 @@ import { WidgetCard, DeltaChip, WidgetEmpty } from './widget-card';
5
5
  import { accentClasses, paletteColor, formatWidgetValue, formatAxisTick, formatDelta, CHART_GRID, CHART_MUTED, } from './widget-format';
6
6
  const fmtCtx = (spec, locale, currency) => ({ format: spec.format, locale, currency });
7
7
  const hasSeries = (d) => Array.isArray(d?.series) && d.series.length > 0;
8
- const CHART_HEIGHT = 132;
8
+ // Chart body that fills the card's flexible height. The dashboard grid gives
9
+ // each chart cell a definite height (fixed auto-rows × row-span), so height:100%
10
+ // resolves cleanly and charts scale with the card instead of a fixed stub.
11
+ function ChartArea({ children }) {
12
+ return (_jsx("div", { className: "min-h-0 flex-1", children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: children }) }));
13
+ }
9
14
  // Compact recharts tooltip styled with theme tokens.
10
15
  function ChartTooltip({ ctx }) {
11
16
  return (_jsx(Tooltip, { cursor: { fill: 'var(--muted, rgba(148,163,184,0.12))', opacity: 0.4 }, contentStyle: {
@@ -23,20 +28,20 @@ export function StatWidget(p) {
23
28
  const value = p.data?.value;
24
29
  const delta = p.data?.delta;
25
30
  const hasValue = typeof value === 'number' && !Number.isNaN(value);
26
- return (_jsx(WidgetCard, { "data-testid": `widget-${p.spec.key}`, title: p.spec.title, subtitle: p.spec.subtitle, icon: p.spec.icon, accent: p.spec.accent, headerExtra: typeof delta === 'number' ? (_jsx(DeltaChip, { delta: delta, text: formatDelta(delta, p.locale) })) : undefined, children: hasValue ? (_jsx("div", { className: "text-3xl font-semibold tabular-nums tracking-tight text-foreground", children: formatWidgetValue(value, ctx) })) : (_jsx(WidgetEmpty, { message: p.emptyText })) }));
31
+ return (_jsx(WidgetCard, { "data-testid": `widget-${p.spec.key}`, title: p.spec.title, subtitle: p.spec.subtitle, icon: p.spec.icon, accent: p.spec.accent, headerExtra: typeof delta === 'number' ? (_jsx(DeltaChip, { delta: delta, text: formatDelta(delta, p.locale) })) : undefined, children: hasValue ? (_jsx("div", { className: "flex min-h-0 flex-1 flex-col justify-center", children: _jsx("div", { className: "text-[2rem] font-semibold leading-none tabular-nums tracking-tight text-foreground", children: formatWidgetValue(value, ctx) }) })) : (_jsx(WidgetEmpty, { message: p.emptyText })) }));
27
32
  }
28
33
  // --- bar ------------------------------------------------------------------
29
34
  export function BarWidget(p) {
30
35
  const ctx = fmtCtx(p.spec, p.locale, p.currency);
31
36
  const a = accentClasses(p.spec.accent);
32
- return (_jsx(WidgetCard, { "data-testid": `widget-${p.spec.key}`, title: p.spec.title, subtitle: p.spec.subtitle, icon: p.spec.icon, accent: p.spec.accent, children: hasSeries(p.data) ? (_jsx(ResponsiveContainer, { width: "100%", height: CHART_HEIGHT, children: _jsxs(BarChart, { data: p.data.series, margin: { top: 4, right: 4, left: -16, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, stroke: CHART_GRID, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "label", tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, interval: "preserveStartEnd" }), _jsx(YAxis, { tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, width: 44, tickFormatter: (v) => formatAxisTick(v, ctx) }), _jsx(ChartTooltip, { ctx: ctx }), _jsx(Bar, { dataKey: "value", fill: a.chartVar, radius: [4, 4, 0, 0], maxBarSize: 40 })] }) })) : (_jsx(WidgetEmpty, { message: p.emptyText })) }));
37
+ return (_jsx(WidgetCard, { "data-testid": `widget-${p.spec.key}`, title: p.spec.title, subtitle: p.spec.subtitle, icon: p.spec.icon, accent: p.spec.accent, children: hasSeries(p.data) ? (_jsx(ChartArea, { children: _jsxs(BarChart, { data: p.data.series, margin: { top: 4, right: 4, left: -16, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, stroke: CHART_GRID, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "label", tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, interval: "preserveStartEnd" }), _jsx(YAxis, { tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, width: 44, tickFormatter: (v) => formatAxisTick(v, ctx) }), _jsx(ChartTooltip, { ctx: ctx }), _jsx(Bar, { dataKey: "value", fill: a.chartVar, radius: [4, 4, 0, 0], maxBarSize: 48 })] }) })) : (_jsx(WidgetEmpty, { message: p.emptyText })) }));
33
38
  }
34
39
  // --- line / area (shared) -------------------------------------------------
35
40
  function TimeSeriesWidget(p) {
36
41
  const ctx = fmtCtx(p.spec, p.locale, p.currency);
37
42
  const a = accentClasses(p.spec.accent);
38
43
  const gradId = `wg-grad-${p.spec.key}`;
39
- return (_jsx(WidgetCard, { "data-testid": `widget-${p.spec.key}`, title: p.spec.title, subtitle: p.spec.subtitle, icon: p.spec.icon, accent: p.spec.accent, children: hasSeries(p.data) ? (_jsx(ResponsiveContainer, { width: "100%", height: CHART_HEIGHT, children: p.variant === 'area' ? (_jsxs(AreaChart, { data: p.data.series, margin: { top: 4, right: 6, left: -16, bottom: 0 }, children: [_jsx("defs", { children: _jsxs("linearGradient", { id: gradId, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: a.chartVar, stopOpacity: 0.35 }), _jsx("stop", { offset: "100%", stopColor: a.chartVar, stopOpacity: 0.02 })] }) }), _jsx(CartesianGrid, { vertical: false, stroke: CHART_GRID, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "label", tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, interval: "preserveStartEnd" }), _jsx(YAxis, { tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, width: 44, tickFormatter: (v) => formatAxisTick(v, ctx) }), _jsx(ChartTooltip, { ctx: ctx }), _jsx(Area, { type: "monotone", dataKey: "value", stroke: a.chartVar, strokeWidth: 2, fill: `url(#${gradId})` })] })) : (_jsxs(LineChart, { data: p.data.series, margin: { top: 4, right: 6, left: -16, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, stroke: CHART_GRID, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "label", tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, interval: "preserveStartEnd" }), _jsx(YAxis, { tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, width: 44, tickFormatter: (v) => formatAxisTick(v, ctx) }), _jsx(ChartTooltip, { ctx: ctx }), _jsx(Line, { type: "monotone", dataKey: "value", stroke: a.chartVar, strokeWidth: 2, dot: false, activeDot: { r: 4 } })] })) })) : (_jsx(WidgetEmpty, { message: p.emptyText })) }));
44
+ return (_jsx(WidgetCard, { "data-testid": `widget-${p.spec.key}`, title: p.spec.title, subtitle: p.spec.subtitle, icon: p.spec.icon, accent: p.spec.accent, children: hasSeries(p.data) ? (_jsx(ChartArea, { children: p.variant === 'area' ? (_jsxs(AreaChart, { data: p.data.series, margin: { top: 4, right: 6, left: -16, bottom: 0 }, children: [_jsx("defs", { children: _jsxs("linearGradient", { id: gradId, x1: "0", y1: "0", x2: "0", y2: "1", children: [_jsx("stop", { offset: "0%", stopColor: a.chartVar, stopOpacity: 0.35 }), _jsx("stop", { offset: "100%", stopColor: a.chartVar, stopOpacity: 0.02 })] }) }), _jsx(CartesianGrid, { vertical: false, stroke: CHART_GRID, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "label", tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, interval: "preserveStartEnd" }), _jsx(YAxis, { tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, width: 44, tickFormatter: (v) => formatAxisTick(v, ctx) }), _jsx(ChartTooltip, { ctx: ctx }), _jsx(Area, { type: "monotone", dataKey: "value", stroke: a.chartVar, strokeWidth: 2, fill: `url(#${gradId})` })] })) : (_jsxs(LineChart, { data: p.data.series, margin: { top: 4, right: 6, left: -16, bottom: 0 }, children: [_jsx(CartesianGrid, { vertical: false, stroke: CHART_GRID, strokeDasharray: "3 3" }), _jsx(XAxis, { dataKey: "label", tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, interval: "preserveStartEnd" }), _jsx(YAxis, { tick: { fontSize: 10, fill: CHART_MUTED }, tickLine: false, axisLine: false, width: 44, tickFormatter: (v) => formatAxisTick(v, ctx) }), _jsx(ChartTooltip, { ctx: ctx }), _jsx(Line, { type: "monotone", dataKey: "value", stroke: a.chartVar, strokeWidth: 2, dot: false, activeDot: { r: 4 } })] })) })) : (_jsx(WidgetEmpty, { message: p.emptyText })) }));
40
45
  }
41
46
  export function LineWidget(p) {
42
47
  return _jsx(TimeSeriesWidget, { ...p, variant: "line" });
@@ -47,7 +52,7 @@ export function AreaWidget(p) {
47
52
  // --- pie / donut (shared) -------------------------------------------------
48
53
  function CircularWidget(p) {
49
54
  const ctx = fmtCtx(p.spec, p.locale, p.currency);
50
- return (_jsx(WidgetCard, { "data-testid": `widget-${p.spec.key}`, title: p.spec.title, subtitle: p.spec.subtitle, icon: p.spec.icon, accent: p.spec.accent, children: hasSeries(p.data) ? (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(ResponsiveContainer, { width: "50%", height: CHART_HEIGHT, children: _jsxs(PieChart, { children: [_jsx(ChartTooltip, { ctx: ctx }), _jsx(Pie, { data: p.data.series, dataKey: "value", nameKey: "label", innerRadius: p.variant === 'donut' ? 32 : 0, outerRadius: 56, paddingAngle: p.variant === 'donut' ? 2 : 0, stroke: "var(--background, #fff)", strokeWidth: 2, children: p.data.series.map((_, i) => (_jsx(Cell, { fill: paletteColor(i) }, i))) })] }) }), _jsx("ul", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: p.data.series.slice(0, 6).map((pt, i) => (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { className: "size-2.5 shrink-0 rounded-sm", style: { background: paletteColor(i) } }), _jsx("span", { className: "truncate text-muted-foreground", children: pt.label }), _jsx("span", { className: "ml-auto shrink-0 font-medium tabular-nums text-foreground", children: formatWidgetValue(pt.value, ctx) })] }, pt.key))) })] })) : (_jsx(WidgetEmpty, { message: p.emptyText })) }));
55
+ return (_jsx(WidgetCard, { "data-testid": `widget-${p.spec.key}`, title: p.spec.title, subtitle: p.spec.subtitle, icon: p.spec.icon, accent: p.spec.accent, children: hasSeries(p.data) ? (_jsxs("div", { className: "flex min-h-0 flex-1 items-center gap-3", children: [_jsx("div", { className: "h-full min-h-0 w-[46%] shrink-0", children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(PieChart, { children: [_jsx(ChartTooltip, { ctx: ctx }), _jsx(Pie, { data: p.data.series, dataKey: "value", nameKey: "label", innerRadius: p.variant === 'donut' ? '55%' : 0, outerRadius: "92%", paddingAngle: p.variant === 'donut' ? 2 : 0, stroke: "var(--background, #fff)", strokeWidth: 2, children: p.data.series.map((_, i) => (_jsx(Cell, { fill: paletteColor(i) }, i))) })] }) }) }), _jsx("ul", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: p.data.series.slice(0, 6).map((pt, i) => (_jsxs("li", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { className: "size-2.5 shrink-0 rounded-sm", style: { background: paletteColor(i) } }), _jsx("span", { className: "truncate text-muted-foreground", children: pt.label }), _jsx("span", { className: "ml-auto shrink-0 font-medium tabular-nums text-foreground", children: formatWidgetValue(pt.value, ctx) })] }, pt.key))) })] })) : (_jsx(WidgetEmpty, { message: p.emptyText })) }));
51
56
  }
52
57
  export function PieWidget(p) {
53
58
  return _jsx(CircularWidget, { ...p, variant: "pie" });
@@ -12,7 +12,7 @@ export function WidgetCard({ title, subtitle, icon, accent, headerExtra, childre
12
12
  const showIcon = icon && isLucideIconName(icon);
13
13
  return (_jsxs(Card, { ...rest, className: cn(
14
14
  // base: subtle border, ring on hover, gentle lift, mount fade-in
15
- 'group/widget relative flex h-full flex-col overflow-hidden', 'border-border/60 transition-all duration-200', 'hover:border-border hover:ring-1 hover:ring-ring/30 hover:shadow-sm', 'motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-1 motion-safe:duration-500', className), children: [_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-3 space-y-0 pb-2", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [showIcon && (_jsx("span", { className: cn('flex size-9 shrink-0 items-center justify-center rounded-lg', a.chip), children: _jsx(DynamicIcon, { name: icon, className: "size-[18px]" }) })), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-medium leading-tight text-foreground", children: title }), subtitle && (_jsx("div", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: subtitle }))] })] }), headerExtra && _jsx("div", { className: "shrink-0", children: headerExtra })] }), _jsx(CardContent, { className: "flex flex-1 flex-col justify-end pt-1", children: children })] }));
15
+ 'group/widget relative flex h-full flex-col overflow-hidden', 'border-border/60 transition-all duration-200', 'hover:border-border hover:ring-1 hover:ring-ring/30 hover:shadow-sm', 'motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-1 motion-safe:duration-500', className), children: [_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-3 space-y-0 pb-2", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [showIcon && (_jsx("span", { className: cn('flex size-9 shrink-0 items-center justify-center rounded-lg', a.chip), children: _jsx(DynamicIcon, { name: icon, className: "size-[18px]" }) })), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate text-sm font-medium leading-tight text-foreground", children: title }), subtitle && (_jsx("div", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: subtitle }))] })] }), headerExtra && _jsx("div", { className: "shrink-0", children: headerExtra })] }), _jsx(CardContent, { className: "flex min-h-0 flex-1 flex-col pt-1", children: children })] }));
16
16
  }
17
17
  /** Delta chip: green up / red down, neutral on zero. `text` is preformatted. */
18
18
  export function DeltaChip({ delta, text }) {
@@ -2,8 +2,15 @@ import * as React from 'react';
2
2
  import type { DashboardWidgetSpec, WidgetData, WidgetSize } from '../dashboard-types';
3
3
  /** Maps a widget size to its column span in the 4-col grid. */
4
4
  export declare const SIZE_SPAN: Record<WidgetSize, number>;
5
- /** Tailwind col-span class per size (static → scanned in this package build). */
5
+ /** Tailwind col-span class per size (static → scanned in this package build).
6
+ * Grid is 2 cols on mobile, 4 on lg: sm=quarter, md=half, lg=¾, full=row. */
6
7
  export declare const SIZE_CLASS: Record<WidgetSize, string>;
8
+ /** Default footprint when a spec omits `size`: charts go half-width, stats a
9
+ * quarter — so a tablero of mixed widgets packs densely without manual sizing. */
10
+ export declare function defaultSize(spec: DashboardWidgetSpec): WidgetSize;
11
+ /** Combined col + row span for a widget's grid cell. Row-span drives the
12
+ * height contrast (chart=2, stat=1) that makes the layout feel designed. */
13
+ export declare function spanClass(spec: DashboardWidgetSpec): string;
7
14
  export interface WidgetRendererProps {
8
15
  spec: DashboardWidgetSpec;
9
16
  data?: WidgetData;
@@ -1 +1 @@
1
- {"version":3,"file":"widget-renderer.d.ts","sourceRoot":"","sources":["../../src/widgets/widget-renderer.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAcrF,+DAA+D;AAC/D,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAKhD,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAKjD,CAAA;AAiDD,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,mBAAmB,CAAA;IACzB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC3B,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,SAAS,EACT,SAAS,GACZ,EAAE,mBAAmB,qBAkDrB;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,qBAqBrE"}
1
+ {"version":3,"file":"widget-renderer.d.ts","sourceRoot":"","sources":["../../src/widgets/widget-renderer.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAcrF,+DAA+D;AAC/D,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAKhD,CAAA;AAED;6EAC6E;AAC7E,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAKjD,CAAA;AAMD;kFACkF;AAClF,wBAAgB,WAAW,CAAC,IAAI,EAAE,mBAAmB,GAAG,UAAU,CAGjE;AAED;4EAC4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAG3D;AAiDD,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,mBAAmB,CAAA;IACzB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAA;CACpB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC3B,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,SAAS,EACT,SAAS,GACZ,EAAE,mBAAmB,qBAkDrB;AAED,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,qBAqBrE"}
@@ -13,13 +13,30 @@ export const SIZE_SPAN = {
13
13
  lg: 3,
14
14
  full: 4,
15
15
  };
16
- /** Tailwind col-span class per size (static → scanned in this package build). */
16
+ /** Tailwind col-span class per size (static → scanned in this package build).
17
+ * Grid is 2 cols on mobile, 4 on lg: sm=quarter, md=half, lg=¾, full=row. */
17
18
  export const SIZE_CLASS = {
18
- sm: 'sm:col-span-2 lg:col-span-1',
19
- md: 'sm:col-span-2 lg:col-span-2',
20
- lg: 'sm:col-span-2 lg:col-span-3',
21
- full: 'sm:col-span-2 lg:col-span-4',
19
+ sm: 'col-span-1 lg:col-span-1',
20
+ md: 'col-span-2 lg:col-span-2',
21
+ lg: 'col-span-2 lg:col-span-3',
22
+ full: 'col-span-2 lg:col-span-4',
22
23
  };
24
+ /** Kinds that render a chart/list and want extra vertical room (2 grid rows).
25
+ * Stat/progress stay a single compact row so KPIs read like KPIs, not slabs. */
26
+ const TALL_KINDS = new Set(['bar', 'line', 'area', 'pie', 'donut', 'list', 'custom']);
27
+ /** Default footprint when a spec omits `size`: charts go half-width, stats a
28
+ * quarter — so a tablero of mixed widgets packs densely without manual sizing. */
29
+ export function defaultSize(spec) {
30
+ if (spec.size)
31
+ return spec.size;
32
+ return TALL_KINDS.has(spec.kind) ? 'md' : 'sm';
33
+ }
34
+ /** Combined col + row span for a widget's grid cell. Row-span drives the
35
+ * height contrast (chart=2, stat=1) that makes the layout feel designed. */
36
+ export function spanClass(spec) {
37
+ const col = SIZE_CLASS[defaultSize(spec)];
38
+ return `${col} ${TALL_KINDS.has(spec.kind) ? 'row-span-2' : 'row-span-1'}`;
39
+ }
23
40
  const RENDERERS = {
24
41
  stat: StatWidget,
25
42
  bar: BarWidget,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.17.0",
3
+ "version": "18.17.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,7 @@ import type {
15
15
  DashboardWidgetGroup,
16
16
  DashboardWidgetSpec,
17
17
  } from './dashboard-types'
18
- import { WidgetRenderer, WidgetSkeleton, SIZE_CLASS } from './widgets/widget-renderer'
18
+ import { WidgetRenderer, WidgetSkeleton, spanClass } from './widgets/widget-renderer'
19
19
 
20
20
  const DEFAULT_STRINGS: DashboardGridStrings = {
21
21
  emptyTitle: 'Your dashboard is empty',
@@ -164,11 +164,10 @@ export function DashboardGrid({
164
164
  {tr(group.title, group.title)}
165
165
  </h2>
166
166
  )}
167
- <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
167
+ <div className="grid grid-flow-row-dense auto-rows-[116px] grid-cols-2 gap-4 lg:grid-cols-4">
168
168
  {group.widgets.map((spec) => {
169
- const size = spec.size ?? 'sm'
170
169
  return (
171
- <div key={spec.key} className={SIZE_CLASS[size]}>
170
+ <div key={spec.key} className={cn(spanClass(spec), 'min-h-0')}>
172
171
  {loading ? (
173
172
  <WidgetSkeleton
174
173
  spec={{
@@ -53,7 +53,18 @@ const fmtCtx = (
53
53
  const hasSeries = (d?: WidgetData): d is WidgetData & { series: NonNullable<WidgetData['series']> } =>
54
54
  Array.isArray(d?.series) && d!.series!.length > 0
55
55
 
56
- const CHART_HEIGHT = 132
56
+ // Chart body that fills the card's flexible height. The dashboard grid gives
57
+ // each chart cell a definite height (fixed auto-rows × row-span), so height:100%
58
+ // resolves cleanly and charts scale with the card instead of a fixed stub.
59
+ function ChartArea({ children }: { children: React.ReactElement }) {
60
+ return (
61
+ <div className="min-h-0 flex-1">
62
+ <ResponsiveContainer width="100%" height="100%">
63
+ {children}
64
+ </ResponsiveContainer>
65
+ </div>
66
+ )
67
+ }
57
68
 
58
69
  // Compact recharts tooltip styled with theme tokens.
59
70
  function ChartTooltip({ ctx }: { ctx: WidgetFormatCtx }) {
@@ -94,8 +105,10 @@ export function StatWidget(p: WidgetRenderProps) {
94
105
  }
95
106
  >
96
107
  {hasValue ? (
97
- <div className="text-3xl font-semibold tabular-nums tracking-tight text-foreground">
98
- {formatWidgetValue(value!, ctx)}
108
+ <div className="flex min-h-0 flex-1 flex-col justify-center">
109
+ <div className="text-[2rem] font-semibold leading-none tabular-nums tracking-tight text-foreground">
110
+ {formatWidgetValue(value!, ctx)}
111
+ </div>
99
112
  </div>
100
113
  ) : (
101
114
  <WidgetEmpty message={p.emptyText} />
@@ -117,7 +130,7 @@ export function BarWidget(p: WidgetRenderProps) {
117
130
  accent={p.spec.accent}
118
131
  >
119
132
  {hasSeries(p.data) ? (
120
- <ResponsiveContainer width="100%" height={CHART_HEIGHT}>
133
+ <ChartArea>
121
134
  <BarChart data={p.data.series} margin={{ top: 4, right: 4, left: -16, bottom: 0 }}>
122
135
  <CartesianGrid vertical={false} stroke={CHART_GRID} strokeDasharray="3 3" />
123
136
  <XAxis
@@ -135,9 +148,9 @@ export function BarWidget(p: WidgetRenderProps) {
135
148
  tickFormatter={(v: number) => formatAxisTick(v, ctx)}
136
149
  />
137
150
  <ChartTooltip ctx={ctx} />
138
- <Bar dataKey="value" fill={a.chartVar} radius={[4, 4, 0, 0]} maxBarSize={40} />
151
+ <Bar dataKey="value" fill={a.chartVar} radius={[4, 4, 0, 0]} maxBarSize={48} />
139
152
  </BarChart>
140
- </ResponsiveContainer>
153
+ </ChartArea>
141
154
  ) : (
142
155
  <WidgetEmpty message={p.emptyText} />
143
156
  )}
@@ -159,7 +172,7 @@ function TimeSeriesWidget(p: WidgetRenderProps & { variant: 'line' | 'area' }) {
159
172
  accent={p.spec.accent}
160
173
  >
161
174
  {hasSeries(p.data) ? (
162
- <ResponsiveContainer width="100%" height={CHART_HEIGHT}>
175
+ <ChartArea>
163
176
  {p.variant === 'area' ? (
164
177
  <AreaChart data={p.data.series} margin={{ top: 4, right: 6, left: -16, bottom: 0 }}>
165
178
  <defs>
@@ -183,7 +196,7 @@ function TimeSeriesWidget(p: WidgetRenderProps & { variant: 'line' | 'area' }) {
183
196
  <Line type="monotone" dataKey="value" stroke={a.chartVar} strokeWidth={2} dot={false} activeDot={{ r: 4 }} />
184
197
  </LineChart>
185
198
  )}
186
- </ResponsiveContainer>
199
+ </ChartArea>
187
200
  ) : (
188
201
  <WidgetEmpty message={p.emptyText} />
189
202
  )}
@@ -210,26 +223,28 @@ function CircularWidget(p: WidgetRenderProps & { variant: 'pie' | 'donut' }) {
210
223
  accent={p.spec.accent}
211
224
  >
212
225
  {hasSeries(p.data) ? (
213
- <div className="flex items-center gap-3">
214
- <ResponsiveContainer width="50%" height={CHART_HEIGHT}>
215
- <PieChart>
216
- <ChartTooltip ctx={ctx} />
217
- <Pie
218
- data={p.data.series}
219
- dataKey="value"
220
- nameKey="label"
221
- innerRadius={p.variant === 'donut' ? 32 : 0}
222
- outerRadius={56}
223
- paddingAngle={p.variant === 'donut' ? 2 : 0}
224
- stroke="var(--background, #fff)"
225
- strokeWidth={2}
226
- >
227
- {p.data.series.map((_, i) => (
228
- <Cell key={i} fill={paletteColor(i)} />
229
- ))}
230
- </Pie>
231
- </PieChart>
232
- </ResponsiveContainer>
226
+ <div className="flex min-h-0 flex-1 items-center gap-3">
227
+ <div className="h-full min-h-0 w-[46%] shrink-0">
228
+ <ResponsiveContainer width="100%" height="100%">
229
+ <PieChart>
230
+ <ChartTooltip ctx={ctx} />
231
+ <Pie
232
+ data={p.data.series}
233
+ dataKey="value"
234
+ nameKey="label"
235
+ innerRadius={p.variant === 'donut' ? '55%' : 0}
236
+ outerRadius="92%"
237
+ paddingAngle={p.variant === 'donut' ? 2 : 0}
238
+ stroke="var(--background, #fff)"
239
+ strokeWidth={2}
240
+ >
241
+ {p.data.series.map((_, i) => (
242
+ <Cell key={i} fill={paletteColor(i)} />
243
+ ))}
244
+ </Pie>
245
+ </PieChart>
246
+ </ResponsiveContainer>
247
+ </div>
233
248
  <ul className="flex min-w-0 flex-1 flex-col gap-1.5">
234
249
  {p.data.series.slice(0, 6).map((pt, i) => (
235
250
  <li key={pt.key} className="flex items-center gap-2 text-xs">
@@ -78,7 +78,7 @@ export function WidgetCard({
78
78
  </div>
79
79
  {headerExtra && <div className="shrink-0">{headerExtra}</div>}
80
80
  </CardHeader>
81
- <CardContent className="flex flex-1 flex-col justify-end pt-1">
81
+ <CardContent className="flex min-h-0 flex-1 flex-col pt-1">
82
82
  {children}
83
83
  </CardContent>
84
84
  </Card>
@@ -26,12 +26,31 @@ export const SIZE_SPAN: Record<WidgetSize, number> = {
26
26
  full: 4,
27
27
  }
28
28
 
29
- /** Tailwind col-span class per size (static → scanned in this package build). */
29
+ /** Tailwind col-span class per size (static → scanned in this package build).
30
+ * Grid is 2 cols on mobile, 4 on lg: sm=quarter, md=half, lg=¾, full=row. */
30
31
  export const SIZE_CLASS: Record<WidgetSize, string> = {
31
- sm: 'sm:col-span-2 lg:col-span-1',
32
- md: 'sm:col-span-2 lg:col-span-2',
33
- lg: 'sm:col-span-2 lg:col-span-3',
34
- full: 'sm:col-span-2 lg:col-span-4',
32
+ sm: 'col-span-1 lg:col-span-1',
33
+ md: 'col-span-2 lg:col-span-2',
34
+ lg: 'col-span-2 lg:col-span-3',
35
+ full: 'col-span-2 lg:col-span-4',
36
+ }
37
+
38
+ /** Kinds that render a chart/list and want extra vertical room (2 grid rows).
39
+ * Stat/progress stay a single compact row so KPIs read like KPIs, not slabs. */
40
+ const TALL_KINDS = new Set(['bar', 'line', 'area', 'pie', 'donut', 'list', 'custom'])
41
+
42
+ /** Default footprint when a spec omits `size`: charts go half-width, stats a
43
+ * quarter — so a tablero of mixed widgets packs densely without manual sizing. */
44
+ export function defaultSize(spec: DashboardWidgetSpec): WidgetSize {
45
+ if (spec.size) return spec.size
46
+ return TALL_KINDS.has(spec.kind) ? 'md' : 'sm'
47
+ }
48
+
49
+ /** Combined col + row span for a widget's grid cell. Row-span drives the
50
+ * height contrast (chart=2, stat=1) that makes the layout feel designed. */
51
+ export function spanClass(spec: DashboardWidgetSpec): string {
52
+ const col = SIZE_CLASS[defaultSize(spec)]
53
+ return `${col} ${TALL_KINDS.has(spec.kind) ? 'row-span-2' : 'row-span-1'}`
35
54
  }
36
55
 
37
56
  const RENDERERS: Record<