@asteby/metacore-runtime-react 18.17.1 → 18.17.2
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 +27 -14
- package/dist/widgets/renderers.d.ts.map +1 -1
- package/dist/widgets/renderers.js +8 -6
- package/dist/widgets/widget-renderer.d.ts +3 -0
- package/dist/widgets/widget-renderer.d.ts.map +1 -1
- package/dist/widgets/widget-renderer.js +5 -0
- package/package.json +3 -3
- package/src/dashboard-grid.tsx +51 -44
- package/src/widgets/renderers.tsx +11 -7
- package/src/widgets/widget-renderer.tsx +6 -0
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,qBAkJpB"}
|
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, isTallWidget } 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,17 +110,30 @@ 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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
// ONE unified dense grid across every group. Per-group sections used to
|
|
114
|
+
// break the layout into rows, so a lone-widget group (e.g. a single KPI)
|
|
115
|
+
// left the rest of its row blank. Flattening + `grid-flow-row-dense`
|
|
116
|
+
// backfills those holes; ordering compact KPIs before charts makes the top
|
|
117
|
+
// read as a metric band and the charts mosaic below it. No blank space.
|
|
118
|
+
const ordered = React.useMemo(() => {
|
|
119
|
+
const flat = visibleGroups.flatMap((g) => g.widgets);
|
|
120
|
+
return flat
|
|
121
|
+
.map((w, i) => ({ w, i }))
|
|
122
|
+
.sort((a, b) => (isTallWidget(a.w) ? 1 : 0) - (isTallWidget(b.w) ? 1 : 0) ||
|
|
123
|
+
a.i - b.i)
|
|
124
|
+
.map((x) => x.w);
|
|
125
|
+
}, [visibleGroups]);
|
|
126
|
+
return (_jsx("div", { "data-testid": "dashboard-grid", className: cn(
|
|
127
|
+
// Masonry: balanced CSS columns. Cards take their natural height
|
|
128
|
+
// (compact stats, taller charts) and flow to equalize column
|
|
129
|
+
// height, so there are NO blank cells — the gaps a fixed grid
|
|
130
|
+
// leaves around mixed 1×1 / 2×2 tiles simply don't exist here.
|
|
131
|
+
'columns-1 gap-4 md:columns-2 xl:columns-3', className), children: ordered.map((spec) => {
|
|
132
|
+
const rspec = {
|
|
133
|
+
...spec,
|
|
134
|
+
title: tr(spec.title, spec.title),
|
|
135
|
+
subtitle: tr(spec.subtitle, spec.subtitle),
|
|
136
|
+
};
|
|
137
|
+
return (_jsx("div", { className: "mb-4 break-inside-avoid", children: loading ? (_jsx(WidgetSkeleton, { spec: rspec })) : (_jsx(WidgetRenderer, { spec: rspec, data: data?.[spec.key], locale: locale, currency: currency, emptyText: spec.empty ? tr(spec.empty, spec.empty) : s.widgetEmpty, errorText: s.widgetError })) }, spec.key));
|
|
138
|
+
}) }));
|
|
126
139
|
}
|
|
@@ -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;AA8CD,wBAAgB,UAAU,CAAC,CAAC,EAAE,iBAAiB,qBA+B9C;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,11 +5,13 @@ 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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// Fixed-height chart body. In the masonry (CSS columns) layout cards take their
|
|
9
|
+
// natural height, so charts need a definite height for ResponsiveContainer to
|
|
10
|
+
// resolve. This height + the card chrome gives every chart card a consistent,
|
|
11
|
+
// taller footprint that balances cleanly against compact stat cards.
|
|
12
|
+
const CHART_H = 'h-[190px]';
|
|
11
13
|
function ChartArea({ children }) {
|
|
12
|
-
return (_jsx("div", { className:
|
|
14
|
+
return (_jsx("div", { className: cn(CHART_H, 'w-full'), children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: children }) }));
|
|
13
15
|
}
|
|
14
16
|
// Compact recharts tooltip styled with theme tokens.
|
|
15
17
|
function ChartTooltip({ ctx }) {
|
|
@@ -28,7 +30,7 @@ export function StatWidget(p) {
|
|
|
28
30
|
const value = p.data?.value;
|
|
29
31
|
const delta = p.data?.delta;
|
|
30
32
|
const hasValue = typeof value === 'number' && !Number.isNaN(value);
|
|
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-
|
|
33
|
+
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-[2.5rem] items-end", children: _jsx("div", { className: "text-[2rem] font-semibold leading-none tabular-nums tracking-tight text-foreground", children: formatWidgetValue(value, ctx) }) })) : (_jsx("div", { className: "flex min-h-[2.5rem] items-center text-sm text-muted-foreground", children: p.emptyText })) }));
|
|
32
34
|
}
|
|
33
35
|
// --- bar ------------------------------------------------------------------
|
|
34
36
|
export function BarWidget(p) {
|
|
@@ -52,7 +54,7 @@ export function AreaWidget(p) {
|
|
|
52
54
|
// --- pie / donut (shared) -------------------------------------------------
|
|
53
55
|
function CircularWidget(p) {
|
|
54
56
|
const ctx = fmtCtx(p.spec, p.locale, p.currency);
|
|
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:
|
|
57
|
+
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: cn(CHART_H, 'flex 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 })) }));
|
|
56
58
|
}
|
|
57
59
|
export function PieWidget(p) {
|
|
58
60
|
return _jsx(CircularWidget, { ...p, variant: "pie" });
|
|
@@ -11,6 +11,9 @@ export declare function defaultSize(spec: DashboardWidgetSpec): WidgetSize;
|
|
|
11
11
|
/** Combined col + row span for a widget's grid cell. Row-span drives the
|
|
12
12
|
* height contrast (chart=2, stat=1) that makes the layout feel designed. */
|
|
13
13
|
export declare function spanClass(spec: DashboardWidgetSpec): string;
|
|
14
|
+
/** True for chart/list widgets that occupy 2 rows. Used to order compact KPIs
|
|
15
|
+
* before charts in the unified grid so the top reads as a metric band. */
|
|
16
|
+
export declare function isTallWidget(spec: DashboardWidgetSpec): boolean;
|
|
14
17
|
export interface WidgetRendererProps {
|
|
15
18
|
spec: DashboardWidgetSpec;
|
|
16
19
|
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;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"}
|
|
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;AAED;0EAC0E;AAC1E,wBAAgB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAE/D;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"}
|
|
@@ -37,6 +37,11 @@ export function spanClass(spec) {
|
|
|
37
37
|
const col = SIZE_CLASS[defaultSize(spec)];
|
|
38
38
|
return `${col} ${TALL_KINDS.has(spec.kind) ? 'row-span-2' : 'row-span-1'}`;
|
|
39
39
|
}
|
|
40
|
+
/** True for chart/list widgets that occupy 2 rows. Used to order compact KPIs
|
|
41
|
+
* before charts in the unified grid so the top reads as a metric band. */
|
|
42
|
+
export function isTallWidget(spec) {
|
|
43
|
+
return TALL_KINDS.has(spec.kind);
|
|
44
|
+
}
|
|
40
45
|
const RENDERERS = {
|
|
41
46
|
stat: StatWidget,
|
|
42
47
|
bar: BarWidget,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "18.17.
|
|
3
|
+
"version": "18.17.2",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"typescript": "^6.0.0",
|
|
65
65
|
"vitest": "^4.0.0",
|
|
66
66
|
"zustand": "^5.0.0",
|
|
67
|
-
"@asteby/metacore-
|
|
68
|
-
"@asteby/metacore-
|
|
67
|
+
"@asteby/metacore-ui": "2.5.2",
|
|
68
|
+
"@asteby/metacore-sdk": "3.2.0"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc -p tsconfig.json",
|
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, isTallWidget } from './widgets/widget-renderer'
|
|
19
19
|
|
|
20
20
|
const DEFAULT_STRINGS: DashboardGridStrings = {
|
|
21
21
|
emptyTitle: 'Your dashboard is empty',
|
|
@@ -155,51 +155,58 @@ export function DashboardGrid({
|
|
|
155
155
|
)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// ONE unified dense grid across every group. Per-group sections used to
|
|
159
|
+
// break the layout into rows, so a lone-widget group (e.g. a single KPI)
|
|
160
|
+
// left the rest of its row blank. Flattening + `grid-flow-row-dense`
|
|
161
|
+
// backfills those holes; ordering compact KPIs before charts makes the top
|
|
162
|
+
// read as a metric band and the charts mosaic below it. No blank space.
|
|
163
|
+
const ordered = React.useMemo(() => {
|
|
164
|
+
const flat = visibleGroups.flatMap((g) => g.widgets)
|
|
165
|
+
return flat
|
|
166
|
+
.map((w, i) => ({ w, i }))
|
|
167
|
+
.sort(
|
|
168
|
+
(a, b) =>
|
|
169
|
+
(isTallWidget(a.w) ? 1 : 0) - (isTallWidget(b.w) ? 1 : 0) ||
|
|
170
|
+
a.i - b.i,
|
|
171
|
+
)
|
|
172
|
+
.map((x) => x.w)
|
|
173
|
+
}, [visibleGroups])
|
|
174
|
+
|
|
158
175
|
return (
|
|
159
|
-
<div
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
spec.empty
|
|
191
|
-
? tr(spec.empty, spec.empty)
|
|
192
|
-
: s.widgetEmpty
|
|
193
|
-
}
|
|
194
|
-
errorText={s.widgetError}
|
|
195
|
-
/>
|
|
196
|
-
)}
|
|
197
|
-
</div>
|
|
198
|
-
)
|
|
199
|
-
})}
|
|
176
|
+
<div
|
|
177
|
+
data-testid="dashboard-grid"
|
|
178
|
+
className={cn(
|
|
179
|
+
// Masonry: balanced CSS columns. Cards take their natural height
|
|
180
|
+
// (compact stats, taller charts) and flow to equalize column
|
|
181
|
+
// height, so there are NO blank cells — the gaps a fixed grid
|
|
182
|
+
// leaves around mixed 1×1 / 2×2 tiles simply don't exist here.
|
|
183
|
+
'columns-1 gap-4 md:columns-2 xl:columns-3',
|
|
184
|
+
className,
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
{ordered.map((spec) => {
|
|
188
|
+
const rspec = {
|
|
189
|
+
...spec,
|
|
190
|
+
title: tr(spec.title, spec.title),
|
|
191
|
+
subtitle: tr(spec.subtitle, spec.subtitle),
|
|
192
|
+
}
|
|
193
|
+
return (
|
|
194
|
+
<div key={spec.key} className="mb-4 break-inside-avoid">
|
|
195
|
+
{loading ? (
|
|
196
|
+
<WidgetSkeleton spec={rspec} />
|
|
197
|
+
) : (
|
|
198
|
+
<WidgetRenderer
|
|
199
|
+
spec={rspec}
|
|
200
|
+
data={data?.[spec.key]}
|
|
201
|
+
locale={locale}
|
|
202
|
+
currency={currency}
|
|
203
|
+
emptyText={spec.empty ? tr(spec.empty, spec.empty) : s.widgetEmpty}
|
|
204
|
+
errorText={s.widgetError}
|
|
205
|
+
/>
|
|
206
|
+
)}
|
|
200
207
|
</div>
|
|
201
|
-
|
|
202
|
-
)
|
|
208
|
+
)
|
|
209
|
+
})}
|
|
203
210
|
</div>
|
|
204
211
|
)
|
|
205
212
|
}
|
|
@@ -53,12 +53,14 @@ 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
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
56
|
+
// Fixed-height chart body. In the masonry (CSS columns) layout cards take their
|
|
57
|
+
// natural height, so charts need a definite height for ResponsiveContainer to
|
|
58
|
+
// resolve. This height + the card chrome gives every chart card a consistent,
|
|
59
|
+
// taller footprint that balances cleanly against compact stat cards.
|
|
60
|
+
const CHART_H = 'h-[190px]'
|
|
59
61
|
function ChartArea({ children }: { children: React.ReactElement }) {
|
|
60
62
|
return (
|
|
61
|
-
<div className=
|
|
63
|
+
<div className={cn(CHART_H, 'w-full')}>
|
|
62
64
|
<ResponsiveContainer width="100%" height="100%">
|
|
63
65
|
{children}
|
|
64
66
|
</ResponsiveContainer>
|
|
@@ -105,13 +107,15 @@ export function StatWidget(p: WidgetRenderProps) {
|
|
|
105
107
|
}
|
|
106
108
|
>
|
|
107
109
|
{hasValue ? (
|
|
108
|
-
<div className="flex min-h-
|
|
110
|
+
<div className="flex min-h-[2.5rem] items-end">
|
|
109
111
|
<div className="text-[2rem] font-semibold leading-none tabular-nums tracking-tight text-foreground">
|
|
110
112
|
{formatWidgetValue(value!, ctx)}
|
|
111
113
|
</div>
|
|
112
114
|
</div>
|
|
113
115
|
) : (
|
|
114
|
-
<
|
|
116
|
+
<div className="flex min-h-[2.5rem] items-center text-sm text-muted-foreground">
|
|
117
|
+
{p.emptyText}
|
|
118
|
+
</div>
|
|
115
119
|
)}
|
|
116
120
|
</WidgetCard>
|
|
117
121
|
)
|
|
@@ -223,7 +227,7 @@ function CircularWidget(p: WidgetRenderProps & { variant: 'pie' | 'donut' }) {
|
|
|
223
227
|
accent={p.spec.accent}
|
|
224
228
|
>
|
|
225
229
|
{hasSeries(p.data) ? (
|
|
226
|
-
<div className=
|
|
230
|
+
<div className={cn(CHART_H, 'flex items-center gap-3')}>
|
|
227
231
|
<div className="h-full min-h-0 w-[46%] shrink-0">
|
|
228
232
|
<ResponsiveContainer width="100%" height="100%">
|
|
229
233
|
<PieChart>
|
|
@@ -53,6 +53,12 @@ export function spanClass(spec: DashboardWidgetSpec): string {
|
|
|
53
53
|
return `${col} ${TALL_KINDS.has(spec.kind) ? 'row-span-2' : 'row-span-1'}`
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** True for chart/list widgets that occupy 2 rows. Used to order compact KPIs
|
|
57
|
+
* before charts in the unified grid so the top reads as a metric band. */
|
|
58
|
+
export function isTallWidget(spec: DashboardWidgetSpec): boolean {
|
|
59
|
+
return TALL_KINDS.has(spec.kind)
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
const RENDERERS: Record<
|
|
57
63
|
string,
|
|
58
64
|
(p: WidgetRenderProps) => React.ReactElement
|