@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 +6 -0
- package/dist/dashboard-grid.d.ts.map +1 -1
- package/dist/dashboard-grid.js +3 -4
- package/dist/widgets/renderers.d.ts.map +1 -1
- package/dist/widgets/renderers.js +10 -5
- package/dist/widgets/widget-card.js +1 -1
- package/dist/widgets/widget-renderer.d.ts +8 -1
- package/dist/widgets/widget-renderer.d.ts.map +1 -1
- package/dist/widgets/widget-renderer.js +22 -5
- package/package.json +1 -1
- package/src/dashboard-grid.tsx +3 -4
- package/src/widgets/renderers.tsx +43 -28
- package/src/widgets/widget-card.tsx +1 -1
- package/src/widgets/widget-renderer.tsx +24 -5
package/CHANGELOG.md
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/dashboard-grid.js
CHANGED
|
@@ -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,
|
|
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-
|
|
114
|
-
|
|
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;
|
|
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
|
-
|
|
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-
|
|
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(
|
|
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(
|
|
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: "
|
|
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
|
|
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
|
|
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: '
|
|
19
|
-
md: '
|
|
20
|
-
lg: '
|
|
21
|
-
full: '
|
|
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
package/src/dashboard-grid.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
DashboardWidgetGroup,
|
|
16
16
|
DashboardWidgetSpec,
|
|
17
17
|
} from './dashboard-types'
|
|
18
|
-
import { WidgetRenderer, WidgetSkeleton,
|
|
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-
|
|
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={
|
|
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
|
-
|
|
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="
|
|
98
|
-
|
|
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
|
-
<
|
|
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={
|
|
151
|
+
<Bar dataKey="value" fill={a.chartVar} radius={[4, 4, 0, 0]} maxBarSize={48} />
|
|
139
152
|
</BarChart>
|
|
140
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
215
|
-
<
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
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: '
|
|
32
|
-
md: '
|
|
33
|
-
lg: '
|
|
34
|
-
full: '
|
|
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<
|