@burdenoff/microfe-bigconsole 2026.613.3 → 2026.613.5
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/dist/bigconsole/components/widgets/WidgetRegistry.js +55 -1
- package/dist/bigconsole/components/widgets/WidgetRegistry.js.map +1 -1
- package/dist/bigconsole/components/widgets/chart/ChartWidget.js +123 -91
- package/dist/bigconsole/components/widgets/chart/ChartWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/chart/charts/AreaChart.js +40 -35
- package/dist/bigconsole/components/widgets/chart/charts/AreaChart.js.map +1 -1
- package/dist/bigconsole/components/widgets/chart/charts/BarChart.js +30 -25
- package/dist/bigconsole/components/widgets/chart/charts/BarChart.js.map +1 -1
- package/dist/bigconsole/components/widgets/chart/charts/ComboChart.js +139 -0
- package/dist/bigconsole/components/widgets/chart/charts/ComboChart.js.map +1 -0
- package/dist/bigconsole/components/widgets/chart/charts/LineChart.js +35 -30
- package/dist/bigconsole/components/widgets/chart/charts/LineChart.js.map +1 -1
- package/dist/bigconsole/components/widgets/chart/charts/ScatterChart.js +128 -0
- package/dist/bigconsole/components/widgets/chart/charts/ScatterChart.js.map +1 -0
- package/dist/bigconsole/components/widgets/chart/charts/WaterfallChart.js +111 -0
- package/dist/bigconsole/components/widgets/chart/charts/WaterfallChart.js.map +1 -0
- package/dist/bigconsole/components/widgets/chart/charts/index.js +4 -0
- package/dist/bigconsole/components/widgets/chart/charts/referenceElements.js +49 -0
- package/dist/bigconsole/components/widgets/chart/charts/referenceElements.js.map +1 -0
- package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js +72 -68
- package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/map-widget/MapWidget.js +113 -44
- package/dist/bigconsole/components/widgets/map-widget/MapWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +87 -86
- package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableCell.js +75 -26
- package/dist/bigconsole/components/widgets/table/TableCell.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableHeader.js +25 -18
- package/dist/bigconsole/components/widgets/table/TableHeader.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableWidget.js +260 -105
- package/dist/bigconsole/components/widgets/table/TableWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/utils/conditionalFormat.js +91 -0
- package/dist/bigconsole/components/widgets/utils/conditionalFormat.js.map +1 -0
- package/dist/bigconsole/hooks/index.js +1 -0
- package/dist/bigconsole/hooks/useDashboardImageExport.js +1 -0
- package/dist/bigconsole/utils/widgetTypeMapping.js +1 -0
- package/dist/bigconsole/utils/widgetTypeMapping.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import r from "
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
1
|
+
import { conditionalStyleToClassName as e, resolveConditionalStyle as t } from "../utils/conditionalFormat.js";
|
|
2
|
+
import { firstObjectRow as n, inferValueField as r, unwrapRows as i } from "../utils/inferFields.js";
|
|
3
|
+
import a from "./TrendIndicator.js";
|
|
4
|
+
import o from "./ComparisonBadge.js";
|
|
5
|
+
import s from "./SparklineChart.js";
|
|
6
|
+
import { memo as c, useCallback as l, useMemo as u } from "react";
|
|
7
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
7
8
|
//#region src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx
|
|
8
|
-
function
|
|
9
|
+
function p(e, t) {
|
|
9
10
|
if (typeof e == "string") return e;
|
|
10
11
|
let { format: n = "number", prefix: r, suffix: i } = t || {}, a;
|
|
11
12
|
switch (n) {
|
|
@@ -30,26 +31,26 @@ function d(e, t) {
|
|
|
30
31
|
}
|
|
31
32
|
return r && (a = `${r}${a}`), i && (a = `${a}${i}`), a;
|
|
32
33
|
}
|
|
33
|
-
function
|
|
34
|
+
function m(e) {
|
|
34
35
|
return e === void 0 || e === 0 ? "flat" : e > 0 ? "up" : "down";
|
|
35
36
|
}
|
|
36
|
-
function
|
|
37
|
+
function h(e, t) {
|
|
37
38
|
return e === "flat" ? "muted" : (t ? e === "up" : e === "down") ? "success" : "error";
|
|
38
39
|
}
|
|
39
|
-
var
|
|
40
|
-
let
|
|
41
|
-
if (!
|
|
42
|
-
let
|
|
43
|
-
if ("value" in
|
|
44
|
-
let
|
|
45
|
-
if (
|
|
46
|
-
let
|
|
47
|
-
if (
|
|
48
|
-
let t =
|
|
40
|
+
var g = c(function({ widget: c, data: g, onClick: _, onDrilldown: v }) {
|
|
41
|
+
let y = c.config, b = u(() => {
|
|
42
|
+
if (!g) return;
|
|
43
|
+
let e = (e, t) => t.split(".").reduce((e, t) => e && typeof e == "object" ? e[t] : void 0, e);
|
|
44
|
+
if ("value" in g && g.value !== void 0) return g;
|
|
45
|
+
let t = y?.valueField;
|
|
46
|
+
if (t && typeof g == "object") {
|
|
47
|
+
let n = e(g, t);
|
|
48
|
+
if (n !== void 0) {
|
|
49
|
+
let t = y?.previousValueField, r = t ? e(g, t) : void 0;
|
|
49
50
|
return {
|
|
50
|
-
value:
|
|
51
|
-
previousValue:
|
|
52
|
-
changePercent:
|
|
51
|
+
value: n,
|
|
52
|
+
previousValue: r,
|
|
53
|
+
changePercent: r !== void 0 && typeof n == "number" ? (n - r) / r * 100 : void 0
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -63,12 +64,12 @@ var m = o(function({ widget: o, data: m, onClick: h, onDrilldown: g }) {
|
|
|
63
64
|
"totalRevenue",
|
|
64
65
|
"totalOrders"
|
|
65
66
|
];
|
|
66
|
-
for (let e of a) if (typeof
|
|
67
|
-
let t =
|
|
67
|
+
for (let e of a) if (typeof g == "object" && e in g) {
|
|
68
|
+
let t = g[e];
|
|
68
69
|
if (typeof t == "number" || typeof t == "string") return { value: t };
|
|
69
70
|
}
|
|
70
|
-
if (typeof
|
|
71
|
-
let e =
|
|
71
|
+
if (typeof g == "object" && "summary" in g) {
|
|
72
|
+
let e = g.summary;
|
|
72
73
|
if (e && typeof e == "object") {
|
|
73
74
|
for (let t of a) if (t in e) {
|
|
74
75
|
let n = e[t];
|
|
@@ -76,108 +77,108 @@ var m = o(function({ widget: o, data: m, onClick: h, onDrilldown: g }) {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
if (typeof
|
|
80
|
-
let o =
|
|
80
|
+
if (typeof g == "number" || typeof g == "string") return { value: g };
|
|
81
|
+
let o = n(g);
|
|
81
82
|
if (o) {
|
|
82
|
-
let
|
|
83
|
-
if (
|
|
84
|
-
let
|
|
85
|
-
for (let
|
|
86
|
-
let
|
|
87
|
-
if (typeof
|
|
83
|
+
let t = y?.valueField;
|
|
84
|
+
if (t) {
|
|
85
|
+
let n = i(g) ?? [o];
|
|
86
|
+
for (let r of n) if (r && typeof r == "object") {
|
|
87
|
+
let n = e(r, t);
|
|
88
|
+
if (typeof n == "number" || typeof n == "string") return { value: n };
|
|
88
89
|
}
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
|
-
let
|
|
92
|
-
if (
|
|
93
|
-
let e = o[
|
|
92
|
+
let n = r(o);
|
|
93
|
+
if (n) {
|
|
94
|
+
let e = o[n];
|
|
94
95
|
if (typeof e == "number" || typeof e == "string") return { value: e };
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
|
-
}, [
|
|
98
|
-
|
|
99
|
-
value:
|
|
100
|
-
changePercent:
|
|
101
|
-
trend:
|
|
102
|
-
target:
|
|
103
|
-
}) :
|
|
98
|
+
}, [g, y]), x = u(() => b?.trend || m(b?.changePercent), [b?.trend, b?.changePercent]), S = y?.positiveIsGood ?? !0, C = u(() => b?.value ? p(b.value, y) : "—", [b?.value, y]), w = u(() => h(x, S), [x, S]), T = u(() => e(t(b?.value, y?.conditionalRules)), [b?.value, y?.conditionalRules]), E = l(() => {
|
|
99
|
+
v ? v({
|
|
100
|
+
value: b?.value,
|
|
101
|
+
changePercent: b?.changePercent,
|
|
102
|
+
trend: x,
|
|
103
|
+
target: b?.target
|
|
104
|
+
}) : _ && c.drilldowns?.length && _();
|
|
104
105
|
}, [
|
|
105
|
-
|
|
106
|
-
g,
|
|
107
|
-
o.drilldowns,
|
|
106
|
+
_,
|
|
108
107
|
v,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
c.drilldowns,
|
|
109
|
+
b,
|
|
110
|
+
x
|
|
111
|
+
]), D = !!(v || _ && c.drilldowns?.length);
|
|
112
|
+
return /* @__PURE__ */ f("div", {
|
|
112
113
|
className: `
|
|
113
114
|
flex flex-col h-full p-4
|
|
114
|
-
${
|
|
115
|
+
${D ? "cursor-pointer hover:bg-bg-muted/50 transition-colors" : ""}
|
|
115
116
|
`,
|
|
116
|
-
onClick:
|
|
117
|
-
role:
|
|
118
|
-
tabIndex:
|
|
117
|
+
onClick: E,
|
|
118
|
+
role: D ? "button" : void 0,
|
|
119
|
+
tabIndex: D ? 0 : void 0,
|
|
119
120
|
onKeyDown: (e) => {
|
|
120
|
-
|
|
121
|
+
D && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), E());
|
|
121
122
|
},
|
|
122
123
|
children: [
|
|
123
|
-
|
|
124
|
+
y?.subtitle && /* @__PURE__ */ d("p", {
|
|
124
125
|
className: "text-xs text-text-secondary mb-1 truncate",
|
|
125
|
-
children:
|
|
126
|
+
children: y.subtitle
|
|
126
127
|
}),
|
|
127
|
-
/* @__PURE__ */
|
|
128
|
+
/* @__PURE__ */ f("div", {
|
|
128
129
|
className: "flex items-baseline gap-2 mb-2",
|
|
129
|
-
children: [/* @__PURE__ */
|
|
130
|
-
className:
|
|
131
|
-
title: String(
|
|
132
|
-
children:
|
|
133
|
-
}),
|
|
134
|
-
direction:
|
|
130
|
+
children: [/* @__PURE__ */ d("span", {
|
|
131
|
+
className: `text-3xl font-semibold tracking-tight ${T || "text-text-primary"}`,
|
|
132
|
+
title: String(b?.value),
|
|
133
|
+
children: C
|
|
134
|
+
}), y?.showTrend !== !1 && b?.changePercent !== void 0 && /* @__PURE__ */ d(a, {
|
|
135
|
+
direction: x,
|
|
135
136
|
size: "md",
|
|
136
|
-
positiveIsGood:
|
|
137
|
+
positiveIsGood: S
|
|
137
138
|
})]
|
|
138
139
|
}),
|
|
139
|
-
|
|
140
|
+
y?.showComparison !== !1 && b?.changePercent !== void 0 && /* @__PURE__ */ d("div", {
|
|
140
141
|
className: "mb-3",
|
|
141
|
-
children: /* @__PURE__ */
|
|
142
|
-
value:
|
|
143
|
-
label:
|
|
144
|
-
positiveIsGood:
|
|
142
|
+
children: /* @__PURE__ */ d(o, {
|
|
143
|
+
value: b.changePercent,
|
|
144
|
+
label: y?.comparisonLabel,
|
|
145
|
+
positiveIsGood: S,
|
|
145
146
|
size: "sm"
|
|
146
147
|
})
|
|
147
148
|
}),
|
|
148
|
-
|
|
149
|
+
b?.target !== void 0 && typeof b.value == "number" && /* @__PURE__ */ f("div", {
|
|
149
150
|
className: "mb-3",
|
|
150
|
-
children: [/* @__PURE__ */
|
|
151
|
+
children: [/* @__PURE__ */ f("div", {
|
|
151
152
|
className: "flex justify-between text-xs text-text-secondary mb-1",
|
|
152
|
-
children: [/* @__PURE__ */
|
|
153
|
-
}), /* @__PURE__ */
|
|
153
|
+
children: [/* @__PURE__ */ f("span", { children: ["Target: ", p(b.target, y)] }), /* @__PURE__ */ f("span", { children: [(b.value / b.target * 100).toFixed(0), "%"] })]
|
|
154
|
+
}), /* @__PURE__ */ d("div", {
|
|
154
155
|
className: "h-1.5 bg-bg-muted rounded-full overflow-hidden",
|
|
155
|
-
children: /* @__PURE__ */
|
|
156
|
-
className: `h-full rounded-full transition-all ${
|
|
157
|
-
style: { width: `${Math.min(
|
|
156
|
+
children: /* @__PURE__ */ d("div", {
|
|
157
|
+
className: `h-full rounded-full transition-all ${b.value >= b.target ? "bg-state-success" : "bg-action-primary-bg"}`,
|
|
158
|
+
style: { width: `${Math.min(b.value / b.target * 100, 100)}%` }
|
|
158
159
|
})
|
|
159
160
|
})]
|
|
160
161
|
}),
|
|
161
|
-
|
|
162
|
+
y?.showSparkline !== !1 && b?.sparklineData?.length ? /* @__PURE__ */ d("div", {
|
|
162
163
|
className: "mt-auto pt-2",
|
|
163
|
-
children: /* @__PURE__ */
|
|
164
|
-
data:
|
|
164
|
+
children: /* @__PURE__ */ d(s, {
|
|
165
|
+
data: b.sparklineData,
|
|
165
166
|
width: 120,
|
|
166
167
|
height: 32,
|
|
167
|
-
color:
|
|
168
|
+
color: w,
|
|
168
169
|
showFill: !0
|
|
169
170
|
})
|
|
170
171
|
}) : null,
|
|
171
|
-
|
|
172
|
+
D && /* @__PURE__ */ d("div", {
|
|
172
173
|
className: "mt-auto pt-2 flex items-center justify-end",
|
|
173
|
-
children: /* @__PURE__ */
|
|
174
|
+
children: /* @__PURE__ */ f("span", {
|
|
174
175
|
className: "text-xs text-text-secondary flex items-center gap-1",
|
|
175
|
-
children: ["View details", /* @__PURE__ */
|
|
176
|
+
children: ["View details", /* @__PURE__ */ d("svg", {
|
|
176
177
|
className: "w-3 h-3",
|
|
177
178
|
fill: "none",
|
|
178
179
|
stroke: "currentColor",
|
|
179
180
|
viewBox: "0 0 24 24",
|
|
180
|
-
children: /* @__PURE__ */
|
|
181
|
+
children: /* @__PURE__ */ d("path", {
|
|
181
182
|
strokeLinecap: "round",
|
|
182
183
|
strokeLinejoin: "round",
|
|
183
184
|
strokeWidth: 2,
|
|
@@ -190,6 +191,6 @@ var m = o(function({ widget: o, data: m, onClick: h, onDrilldown: g }) {
|
|
|
190
191
|
});
|
|
191
192
|
});
|
|
192
193
|
//#endregion
|
|
193
|
-
export {
|
|
194
|
+
export { g as default };
|
|
194
195
|
|
|
195
196
|
//# sourceMappingURL=MetricCardWidget.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MetricCardWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx"],"sourcesContent":["/**\n * MetricCardWidget Component\n *\n * Displays a single KPI/metric with trend, comparison, and optional sparkline.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, MetricCardConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\nimport { ComparisonBadge } from './ComparisonBadge';\nimport { SparklineChart, type SparklineDataPoint } from './SparklineChart';\nimport { firstObjectRow, inferValueField, unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MetricCardWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Fetched data */\n data?: MetricCardData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives metric data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MetricCardData {\n /** Primary value to display */\n value: number | string;\n /** Previous value for comparison */\n previousValue?: number;\n /** Percentage change from previous */\n changePercent?: number;\n /** Trend direction override */\n trend?: TrendDirection;\n /** Sparkline data points */\n sparklineData?: SparklineDataPoint[];\n /** Target value for comparison */\n target?: number;\n /** Unit label (e.g., \"users\", \"$\") */\n unit?: string;\n /** Whether the metric is a currency */\n isCurrency?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Format a number based on configuration\n */\nfunction formatValue(value: number | string, config?: MetricCardConfig): string {\n if (typeof value === 'string') return value;\n\n const { format = 'number', prefix, suffix } = config || {};\n\n let formatted: string;\n\n switch (format) {\n case 'currency':\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(value);\n break;\n\n case 'percentage':\n formatted = `${(value * 100).toFixed(1)}%`;\n break;\n\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n break;\n\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n\n // Apply prefix/suffix if provided\n if (prefix) formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n\n return formatted;\n}\n\n/**\n * Calculate trend direction from change percent\n */\nfunction calculateTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n/**\n * Get sparkline color based on trend\n */\nfunction getSparklineColor(trend: TrendDirection, positiveIsGood: boolean): 'success' | 'error' | 'muted' {\n if (trend === 'flat') return 'muted';\n const isGood = positiveIsGood ? trend === 'up' : trend === 'down';\n return isGood ? 'success' : 'error';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MetricCardWidget: FC<MetricCardWidgetProps> = memo(function MetricCardWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as MetricCardConfig | undefined;\n\n // Normalize data - extract value from object if valueField is configured\n const data = useMemo((): MetricCardData | undefined => {\n if (!rawData) return undefined;\n\n // Helper to get nested value using dot notation (e.g., \"summary.totalSales\")\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {\n return path.split('.').reduce((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj as unknown);\n };\n\n // If data already has 'value' property, use it directly\n if ('value' in rawData && rawData.value !== undefined) {\n return rawData as MetricCardData;\n }\n\n // If valueField is configured, extract the value from that field (supports dot notation)\n const valueField = (config as unknown as Record<string, unknown> | undefined)?.valueField as string | undefined;\n if (valueField && typeof rawData === 'object') {\n const extractedValue = getNestedValue(rawData as unknown as Record<string, unknown>, valueField);\n if (extractedValue !== undefined) {\n // Also try to extract previousValue if previousValueField is configured\n const previousValueField = (config as unknown as Record<string, unknown> | undefined)?.previousValueField as\n | string\n | undefined;\n const previousValue = previousValueField\n ? (getNestedValue(rawData as unknown as Record<string, unknown>, previousValueField) as number | undefined)\n : undefined;\n\n return {\n value: extractedValue as number | string,\n previousValue,\n changePercent:\n previousValue !== undefined && typeof extractedValue === 'number'\n ? ((extractedValue - previousValue) / previousValue) * 100\n : undefined,\n };\n }\n }\n\n // Try common field names at top level\n const commonFields = ['value', 'total', 'amount', 'count', 'sum', 'totalSales', 'totalRevenue', 'totalOrders'];\n for (const field of commonFields) {\n if (typeof rawData === 'object' && field in rawData) {\n const val = (rawData as unknown as Record<string, unknown>)[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n // Try looking in nested 'summary' object (common pattern)\n if (typeof rawData === 'object' && 'summary' in rawData) {\n const summary = (rawData as Record<string, unknown>).summary as Record<string, unknown> | undefined;\n if (summary && typeof summary === 'object') {\n for (const field of commonFields) {\n if (field in summary) {\n const val = summary[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n }\n }\n\n // If rawData is a number or string directly, use it\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // BOFF-2246 smart default: if rawData is an array (or envelope around an\n // array) with object rows, use the configured valueField if it resolves\n // on any row (supports dotted paths like \"metrics.revenue\") — otherwise\n // pick the first numeric field from row[0]. This path only fires when\n // the earlier top-level extraction didn't find anything, but an explicit\n // `config.valueField` (flat or dotted) must always win over inference.\n const firstRow = firstObjectRow(rawData);\n if (firstRow) {\n const configuredField = (config as unknown as Record<string, unknown> | undefined)?.valueField as\n | string\n | undefined;\n if (configuredField) {\n // Configured field is authoritative. Scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column\n // from row[0] (that would silently misrepresent the user's mapping).\n const rows = unwrapRows(rawData) ?? [firstRow];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolved = getNestedValue(r as Record<string, unknown>, configuredField);\n if (typeof resolved === 'number' || typeof resolved === 'string') {\n return { value: resolved };\n }\n }\n }\n return undefined;\n }\n const inferredField = inferValueField(firstRow);\n if (inferredField) {\n const val = firstRow[inferredField];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n return undefined;\n }, [rawData, config]);\n\n // Calculate derived values\n const trend = useMemo(() => {\n return data?.trend || calculateTrend(data?.changePercent);\n }, [data?.trend, data?.changePercent]);\n\n const positiveIsGood = config?.positiveIsGood ?? true;\n\n const formattedValue = useMemo(() => {\n if (!data?.value) return '—';\n return formatValue(data.value, config);\n }, [data?.value, config]);\n\n const sparklineColor = useMemo(() => {\n return getSparklineColor(trend, positiveIsGood);\n }, [trend, positiveIsGood]);\n\n // Handle click\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown) {\n onDrilldown({\n value: data?.value,\n changePercent: data?.changePercent,\n trend,\n target: data?.target,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, trend]);\n\n // Determine if clickable\n const isClickable = Boolean(onDrilldown || (onClick && widget.drilldowns?.length));\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Subtitle/Label */}\n {config?.subtitle && <p className=\"text-xs text-text-secondary mb-1 truncate\">{config.subtitle}</p>}\n\n {/* Main Value */}\n <div className=\"flex items-baseline gap-2 mb-2\">\n <span className=\"text-3xl font-semibold text-text-primary tracking-tight\" title={String(data?.value)}>\n {formattedValue}\n </span>\n\n {/* Trend Indicator (inline) */}\n {config?.showTrend !== false && data?.changePercent !== undefined && (\n <TrendIndicator direction={trend} size=\"md\" positiveIsGood={positiveIsGood} />\n )}\n </div>\n\n {/* Comparison Badge */}\n {config?.showComparison !== false && data?.changePercent !== undefined && (\n <div className=\"mb-3\">\n <ComparisonBadge\n value={data.changePercent}\n label={config?.comparisonLabel}\n positiveIsGood={positiveIsGood}\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Target Progress */}\n {data?.target !== undefined && typeof data.value === 'number' && (\n <div className=\"mb-3\">\n <div className=\"flex justify-between text-xs text-text-secondary mb-1\">\n <span>Target: {formatValue(data.target, config)}</span>\n <span>{((data.value / data.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1.5 bg-bg-muted rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${\n data.value >= data.target ? 'bg-state-success' : 'bg-action-primary-bg'\n }`}\n style={{\n width: `${Math.min((data.value / data.target) * 100, 100)}%`,\n }}\n />\n </div>\n </div>\n )}\n\n {/* Sparkline */}\n {config?.showSparkline !== false && data?.sparklineData?.length ? (\n <div className=\"mt-auto pt-2\">\n <SparklineChart data={data.sparklineData} width={120} height={32} color={sparklineColor} showFill />\n </div>\n ) : null}\n\n {/* Drilldown Indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-2 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default MetricCardWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MetricCardWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx"],"sourcesContent":["/**\n * MetricCardWidget Component\n *\n * Displays a single KPI/metric with trend, comparison, and optional sparkline.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, MetricCardConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\nimport { ComparisonBadge } from './ComparisonBadge';\nimport { SparklineChart, type SparklineDataPoint } from './SparklineChart';\nimport { firstObjectRow, inferValueField, unwrapRows } from '../utils/inferFields';\nimport { resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MetricCardWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Fetched data */\n data?: MetricCardData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives metric data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MetricCardData {\n /** Primary value to display */\n value: number | string;\n /** Previous value for comparison */\n previousValue?: number;\n /** Percentage change from previous */\n changePercent?: number;\n /** Trend direction override */\n trend?: TrendDirection;\n /** Sparkline data points */\n sparklineData?: SparklineDataPoint[];\n /** Target value for comparison */\n target?: number;\n /** Unit label (e.g., \"users\", \"$\") */\n unit?: string;\n /** Whether the metric is a currency */\n isCurrency?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Format a number based on configuration\n */\nfunction formatValue(value: number | string, config?: MetricCardConfig): string {\n if (typeof value === 'string') return value;\n\n const { format = 'number', prefix, suffix } = config || {};\n\n let formatted: string;\n\n switch (format) {\n case 'currency':\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(value);\n break;\n\n case 'percentage':\n formatted = `${(value * 100).toFixed(1)}%`;\n break;\n\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n break;\n\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n\n // Apply prefix/suffix if provided\n if (prefix) formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n\n return formatted;\n}\n\n/**\n * Calculate trend direction from change percent\n */\nfunction calculateTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n/**\n * Get sparkline color based on trend\n */\nfunction getSparklineColor(trend: TrendDirection, positiveIsGood: boolean): 'success' | 'error' | 'muted' {\n if (trend === 'flat') return 'muted';\n const isGood = positiveIsGood ? trend === 'up' : trend === 'down';\n return isGood ? 'success' : 'error';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MetricCardWidget: FC<MetricCardWidgetProps> = memo(function MetricCardWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as MetricCardConfig | undefined;\n\n // Normalize data - extract value from object if valueField is configured\n const data = useMemo((): MetricCardData | undefined => {\n if (!rawData) return undefined;\n\n // Helper to get nested value using dot notation (e.g., \"summary.totalSales\")\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {\n return path.split('.').reduce((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj as unknown);\n };\n\n // If data already has 'value' property, use it directly\n if ('value' in rawData && rawData.value !== undefined) {\n return rawData as MetricCardData;\n }\n\n // If valueField is configured, extract the value from that field (supports dot notation)\n const valueField = (config as unknown as Record<string, unknown> | undefined)?.valueField as string | undefined;\n if (valueField && typeof rawData === 'object') {\n const extractedValue = getNestedValue(rawData as unknown as Record<string, unknown>, valueField);\n if (extractedValue !== undefined) {\n // Also try to extract previousValue if previousValueField is configured\n const previousValueField = (config as unknown as Record<string, unknown> | undefined)?.previousValueField as\n | string\n | undefined;\n const previousValue = previousValueField\n ? (getNestedValue(rawData as unknown as Record<string, unknown>, previousValueField) as number | undefined)\n : undefined;\n\n return {\n value: extractedValue as number | string,\n previousValue,\n changePercent:\n previousValue !== undefined && typeof extractedValue === 'number'\n ? ((extractedValue - previousValue) / previousValue) * 100\n : undefined,\n };\n }\n }\n\n // Try common field names at top level\n const commonFields = ['value', 'total', 'amount', 'count', 'sum', 'totalSales', 'totalRevenue', 'totalOrders'];\n for (const field of commonFields) {\n if (typeof rawData === 'object' && field in rawData) {\n const val = (rawData as unknown as Record<string, unknown>)[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n // Try looking in nested 'summary' object (common pattern)\n if (typeof rawData === 'object' && 'summary' in rawData) {\n const summary = (rawData as Record<string, unknown>).summary as Record<string, unknown> | undefined;\n if (summary && typeof summary === 'object') {\n for (const field of commonFields) {\n if (field in summary) {\n const val = summary[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n }\n }\n\n // If rawData is a number or string directly, use it\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // BOFF-2246 smart default: if rawData is an array (or envelope around an\n // array) with object rows, use the configured valueField if it resolves\n // on any row (supports dotted paths like \"metrics.revenue\") — otherwise\n // pick the first numeric field from row[0]. This path only fires when\n // the earlier top-level extraction didn't find anything, but an explicit\n // `config.valueField` (flat or dotted) must always win over inference.\n const firstRow = firstObjectRow(rawData);\n if (firstRow) {\n const configuredField = (config as unknown as Record<string, unknown> | undefined)?.valueField as\n | string\n | undefined;\n if (configuredField) {\n // Configured field is authoritative. Scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column\n // from row[0] (that would silently misrepresent the user's mapping).\n const rows = unwrapRows(rawData) ?? [firstRow];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolved = getNestedValue(r as Record<string, unknown>, configuredField);\n if (typeof resolved === 'number' || typeof resolved === 'string') {\n return { value: resolved };\n }\n }\n }\n return undefined;\n }\n const inferredField = inferValueField(firstRow);\n if (inferredField) {\n const val = firstRow[inferredField];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n return undefined;\n }, [rawData, config]);\n\n // Calculate derived values\n const trend = useMemo(() => {\n return data?.trend || calculateTrend(data?.changePercent);\n }, [data?.trend, data?.changePercent]);\n\n const positiveIsGood = config?.positiveIsGood ?? true;\n\n const formattedValue = useMemo(() => {\n if (!data?.value) return '—';\n return formatValue(data.value, config);\n }, [data?.value, config]);\n\n const sparklineColor = useMemo(() => {\n return getSparklineColor(trend, positiveIsGood);\n }, [trend, positiveIsGood]);\n\n // Conditional formatting on the metric value (threshold-driven color, not\n // just trend direction). Falls back to the default primary text color.\n const valueConditionalClass = useMemo(() => {\n const style = resolveConditionalStyle(data?.value, config?.conditionalRules);\n return conditionalStyleToClassName(style);\n }, [data?.value, config?.conditionalRules]);\n\n // Handle click\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown) {\n onDrilldown({\n value: data?.value,\n changePercent: data?.changePercent,\n trend,\n target: data?.target,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, trend]);\n\n // Determine if clickable\n const isClickable = Boolean(onDrilldown || (onClick && widget.drilldowns?.length));\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Subtitle/Label */}\n {config?.subtitle && <p className=\"text-xs text-text-secondary mb-1 truncate\">{config.subtitle}</p>}\n\n {/* Main Value */}\n <div className=\"flex items-baseline gap-2 mb-2\">\n <span\n className={`text-3xl font-semibold tracking-tight ${valueConditionalClass || 'text-text-primary'}`}\n title={String(data?.value)}\n >\n {formattedValue}\n </span>\n\n {/* Trend Indicator (inline) */}\n {config?.showTrend !== false && data?.changePercent !== undefined && (\n <TrendIndicator direction={trend} size=\"md\" positiveIsGood={positiveIsGood} />\n )}\n </div>\n\n {/* Comparison Badge */}\n {config?.showComparison !== false && data?.changePercent !== undefined && (\n <div className=\"mb-3\">\n <ComparisonBadge\n value={data.changePercent}\n label={config?.comparisonLabel}\n positiveIsGood={positiveIsGood}\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Target Progress */}\n {data?.target !== undefined && typeof data.value === 'number' && (\n <div className=\"mb-3\">\n <div className=\"flex justify-between text-xs text-text-secondary mb-1\">\n <span>Target: {formatValue(data.target, config)}</span>\n <span>{((data.value / data.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1.5 bg-bg-muted rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${\n data.value >= data.target ? 'bg-state-success' : 'bg-action-primary-bg'\n }`}\n style={{\n width: `${Math.min((data.value / data.target) * 100, 100)}%`,\n }}\n />\n </div>\n </div>\n )}\n\n {/* Sparkline */}\n {config?.showSparkline !== false && data?.sparklineData?.length ? (\n <div className=\"mt-auto pt-2\">\n <SparklineChart data={data.sparklineData} width={120} height={32} color={sparklineColor} showFill />\n </div>\n ) : null}\n\n {/* Drilldown Indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-2 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default MetricCardWidget;\n"],"mappings":"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"}
|
|
@@ -1,65 +1,114 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { CONDITIONAL_ICON_PATHS as e, conditionalStyleToClassName as t, intentToCssVar as n, resolveConditionalStyle as r } from "../utils/conditionalFormat.js";
|
|
2
|
+
import i from "../metric-card/SparklineChart.js";
|
|
3
|
+
import { memo as a } from "react";
|
|
4
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
3
5
|
//#region src/bigconsole/components/widgets/table/TableCell.tsx
|
|
4
|
-
function
|
|
5
|
-
if (e
|
|
6
|
+
function c(e) {
|
|
7
|
+
if (!Array.isArray(e) || e.length === 0) return null;
|
|
8
|
+
let t = [];
|
|
9
|
+
for (let n of e) typeof n == "number" && Number.isFinite(n) ? t.push(n) : n && typeof n == "object" && typeof n.value == "number" && Number.isFinite(n.value) && t.push(n.value);
|
|
10
|
+
return t.length ? t : null;
|
|
11
|
+
}
|
|
12
|
+
function l(e, t) {
|
|
13
|
+
if (e == null) return /* @__PURE__ */ o("span", {
|
|
6
14
|
className: "text-text-secondary",
|
|
7
15
|
children: "—"
|
|
8
16
|
});
|
|
9
|
-
switch (
|
|
17
|
+
switch (t?.type || "text") {
|
|
10
18
|
case "number": return typeof e == "number" ? new Intl.NumberFormat("en-US").format(e) : String(e);
|
|
11
19
|
case "currency": return typeof e == "number" ? new Intl.NumberFormat("en-US", {
|
|
12
20
|
style: "currency",
|
|
13
|
-
currency:
|
|
21
|
+
currency: t?.currency || "USD"
|
|
14
22
|
}).format(e) : String(e);
|
|
15
23
|
case "percentage": return typeof e == "number" ? `${(e * 100).toFixed(1)}%` : String(e);
|
|
16
24
|
case "date":
|
|
17
|
-
if (e instanceof Date) return new Intl.DateTimeFormat("en-US",
|
|
25
|
+
if (e instanceof Date) return new Intl.DateTimeFormat("en-US", t?.dateOptions || { dateStyle: "medium" }).format(e);
|
|
18
26
|
if (typeof e == "string" || typeof e == "number") {
|
|
19
|
-
let
|
|
20
|
-
if (!isNaN(
|
|
27
|
+
let n = new Date(e);
|
|
28
|
+
if (!isNaN(n.getTime())) return new Intl.DateTimeFormat("en-US", t?.dateOptions || { dateStyle: "medium" }).format(n);
|
|
21
29
|
}
|
|
22
30
|
return String(e);
|
|
23
31
|
case "badge": {
|
|
24
|
-
let
|
|
25
|
-
return /* @__PURE__ */
|
|
32
|
+
let n = String(e);
|
|
33
|
+
return /* @__PURE__ */ o("span", {
|
|
26
34
|
className: `
|
|
27
35
|
inline-flex items-center
|
|
28
36
|
px-2 py-0.5
|
|
29
37
|
text-xs font-medium
|
|
30
38
|
rounded-full
|
|
31
|
-
${
|
|
39
|
+
${t?.badgeColors?.[n] || "bg-bg-muted text-text-primary"}
|
|
32
40
|
`,
|
|
33
|
-
children:
|
|
41
|
+
children: n
|
|
34
42
|
});
|
|
35
43
|
}
|
|
36
|
-
case "link": return /* @__PURE__ */
|
|
44
|
+
case "link": return /* @__PURE__ */ o("span", {
|
|
37
45
|
className: "text-action-primary-bg hover:underline cursor-pointer",
|
|
38
46
|
children: String(e)
|
|
39
47
|
});
|
|
40
48
|
default: return String(e);
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
|
-
var
|
|
44
|
-
|
|
51
|
+
var u = a(function({ value: a, column: u, format: d, rowData: f, conditionalRules: p, dataBarMax: m, stickyLeft: h, cellChart: g, onClick: _ }) {
|
|
52
|
+
let v = r(a, p, f), y = t(v), b = h != null, x = g === "sparkline" ? c(a) : null, S = x ? x.reduce((e, t) => Math.min(e, t), Infinity) : 0, C = x ? x.reduce((e, t) => Math.max(e, t), -Infinity) : 0, w = v?.dataBar && typeof a == "number" && m && m > 0 ? Math.max(0, Math.min(100, Math.abs(a) / m * 100)) : null;
|
|
53
|
+
return /* @__PURE__ */ s("td", {
|
|
45
54
|
className: `
|
|
46
|
-
px-4 py-3
|
|
55
|
+
relative px-4 py-3
|
|
47
56
|
text-sm text-text-primary
|
|
48
57
|
border-b border-border-default
|
|
49
|
-
${
|
|
50
|
-
${
|
|
51
|
-
${
|
|
58
|
+
${u.align === "center" ? "text-center" : ""}
|
|
59
|
+
${u.align === "right" ? "text-right" : "text-left"}
|
|
60
|
+
${_ ? "cursor-pointer hover:text-action-primary-bg" : ""}
|
|
61
|
+
${b ? "sticky z-10 bg-bg-surface" : ""}
|
|
62
|
+
${y}
|
|
52
63
|
`,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
style: b ? {
|
|
65
|
+
left: h,
|
|
66
|
+
width: u.width
|
|
67
|
+
} : void 0,
|
|
68
|
+
onClick: _,
|
|
69
|
+
role: _ ? "button" : void 0,
|
|
70
|
+
tabIndex: _ ? 0 : void 0,
|
|
56
71
|
onKeyDown: (e) => {
|
|
57
|
-
|
|
72
|
+
_ && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), _());
|
|
58
73
|
},
|
|
59
|
-
children:
|
|
74
|
+
children: [w != null && /* @__PURE__ */ o("span", {
|
|
75
|
+
"aria-hidden": "true",
|
|
76
|
+
className: "absolute inset-y-1 left-1 rounded-sm opacity-20 pointer-events-none",
|
|
77
|
+
style: {
|
|
78
|
+
width: `calc(${w}% - 0.5rem)`,
|
|
79
|
+
backgroundColor: n(v?.dataBarColor ?? "info")
|
|
80
|
+
}
|
|
81
|
+
}), x && x.length >= 2 ? /* @__PURE__ */ o("span", {
|
|
82
|
+
className: "relative inline-flex items-center",
|
|
83
|
+
role: "img",
|
|
84
|
+
"aria-label": `Sparkline, ${x.length} points, ${S.toLocaleString()} to ${C.toLocaleString()}`,
|
|
85
|
+
children: /* @__PURE__ */ o(i, {
|
|
86
|
+
data: x.map((e) => ({ value: e })),
|
|
87
|
+
width: 96,
|
|
88
|
+
height: 28,
|
|
89
|
+
color: "primary"
|
|
90
|
+
})
|
|
91
|
+
}) : x && x.length === 1 ? /* @__PURE__ */ o("span", {
|
|
92
|
+
className: "relative inline-flex items-center",
|
|
93
|
+
children: l(x[0], d)
|
|
94
|
+
}) : /* @__PURE__ */ s("span", {
|
|
95
|
+
className: "relative inline-flex items-center gap-1.5",
|
|
96
|
+
children: [v?.icon && /* @__PURE__ */ o("svg", {
|
|
97
|
+
className: "w-3.5 h-3.5 shrink-0",
|
|
98
|
+
fill: "none",
|
|
99
|
+
stroke: "currentColor",
|
|
100
|
+
viewBox: "0 0 24 24",
|
|
101
|
+
children: /* @__PURE__ */ o("path", {
|
|
102
|
+
strokeLinecap: "round",
|
|
103
|
+
strokeLinejoin: "round",
|
|
104
|
+
strokeWidth: 2,
|
|
105
|
+
d: e[v.icon]
|
|
106
|
+
})
|
|
107
|
+
}), l(a, d)]
|
|
108
|
+
})]
|
|
60
109
|
});
|
|
61
110
|
});
|
|
62
111
|
//#endregion
|
|
63
|
-
export {
|
|
112
|
+
export { u as default };
|
|
64
113
|
|
|
65
114
|
//# sourceMappingURL=TableCell.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TableCell.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/table/TableCell.tsx"],"sourcesContent":["/**\n * TableCell Component\n *\n * Table cell with formatting and rendering options.\n */\n\nimport { type FC, memo, type ReactNode } from 'react';\nimport type { ColumnDefinition } from './TableHeader';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type CellFormat = 'text' | 'number' | 'currency' | 'percentage' | 'date' | 'badge' | 'link';\n\nexport interface CellFormatConfig {\n /** Format type */\n type: CellFormat;\n /** Currency code for currency format */\n currency?: string;\n /** Date format options */\n dateOptions?: Intl.DateTimeFormatOptions;\n /** Badge color mapping (value -> color) */\n badgeColors?: Record<string, string>;\n /** Link URL pattern (use {value} as placeholder) */\n linkPattern?: string;\n}\n\nexport interface TableCellProps {\n /** Cell value */\n value: unknown;\n /** Column definition */\n column: ColumnDefinition;\n /** Format configuration */\n format?: CellFormatConfig;\n /** Row data for link patterns */\n rowData?: Record<string, unknown>;\n /** Click handler */\n onClick?: () => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: unknown, format?: CellFormatConfig): ReactNode {\n if (value === null || value === undefined) {\n return <span className=\"text-text-secondary\">—</span>;\n }\n\n const formatType = format?.type || 'text';\n\n switch (formatType) {\n case 'number':\n return typeof value === 'number' ? new Intl.NumberFormat('en-US').format(value) : String(value);\n\n case 'currency':\n return typeof value === 'number'\n ? new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: format?.currency || 'USD',\n }).format(value)\n : String(value);\n\n case 'percentage':\n return typeof value === 'number' ? `${(value * 100).toFixed(1)}%` : String(value);\n\n case 'date':\n if (value instanceof Date) {\n return new Intl.DateTimeFormat('en-US', format?.dateOptions || { dateStyle: 'medium' }).format(value);\n }\n if (typeof value === 'string' || typeof value === 'number') {\n const date = new Date(value);\n if (!isNaN(date.getTime())) {\n return new Intl.DateTimeFormat('en-US', format?.dateOptions || { dateStyle: 'medium' }).format(date);\n }\n }\n return String(value);\n\n case 'badge': {\n const stringValue = String(value);\n const badgeColor = format?.badgeColors?.[stringValue] || 'bg-bg-muted text-text-primary';\n return (\n <span\n className={`\n inline-flex items-center\n px-2 py-0.5\n text-xs font-medium\n rounded-full\n ${badgeColor}\n `}\n >\n {stringValue}\n </span>\n );\n }\n\n case 'link':\n return <span className=\"text-action-primary-bg hover:underline cursor-pointer\">{String(value)}</span>;\n\n case 'text':\n default:\n return String(value);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TableCell: FC<TableCellProps> = memo(function TableCell({ value, column, format, onClick }) {\n return (\n <td\n className={`\n px-4 py-3\n text-sm text-text-primary\n border-b border-border-default\n ${column.align === 'center' ? 'text-center' : ''}\n ${column.align === 'right' ? 'text-right' : 'text-left'}\n ${onClick ? 'cursor-pointer hover:text-action-primary-bg' : ''}\n `}\n onClick={onClick}\n role={onClick ? 'button' : undefined}\n tabIndex={onClick ? 0 : undefined}\n onKeyDown={(e) => {\n if (onClick && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n onClick();\n }\n }}\n >\n {formatValue(value, format)}\n </td>\n );\n});\n\nexport default TableCell;\n"],"mappings":";;;AA6CA,SAAS,EAAY,GAAgB,GAAsC;AACzE,KAAI,KAAU,KACZ,QAAO,kBAAC,QAAD;EAAM,WAAU;YAAsB;EAAQ,CAAA;AAKvD,SAFmB,GAAQ,QAAQ,QAEnC;EACE,KAAK,SACH,QAAO,OAAO,KAAU,WAAW,IAAI,KAAK,aAAa,QAAQ,CAAC,OAAO,EAAM,GAAG,OAAO,EAAM;EAEjG,KAAK,WACH,QAAO,OAAO,KAAU,WACpB,IAAI,KAAK,aAAa,SAAS;GAC7B,OAAO;GACP,UAAU,GAAQ,YAAY;GAC/B,CAAC,CAAC,OAAO,EAAM,GAChB,OAAO,EAAM;EAEnB,KAAK,aACH,QAAO,OAAO,KAAU,WAAW,IAAI,IAAQ,KAAK,QAAQ,EAAE,CAAC,KAAK,OAAO,EAAM;EAEnF,KAAK;AACH,OAAI,aAAiB,KACnB,QAAO,IAAI,KAAK,eAAe,SAAS,GAAQ,eAAe,EAAE,WAAW,UAAU,CAAC,CAAC,OAAO,EAAM;AAEvG,OAAI,OAAO,KAAU,YAAY,OAAO,KAAU,UAAU;IAC1D,IAAM,IAAO,IAAI,KAAK,EAAM;AAC5B,QAAI,CAAC,MAAM,EAAK,SAAS,CAAC,CACxB,QAAO,IAAI,KAAK,eAAe,SAAS,GAAQ,eAAe,EAAE,WAAW,UAAU,CAAC,CAAC,OAAO,EAAK;;AAGxG,UAAO,OAAO,EAAM;EAEtB,KAAK,SAAS;GACZ,IAAM,IAAc,OAAO,EAAM;AAEjC,UACE,kBAAC,QAAD;IACE,WAAW;;;;;cAHI,GAAQ,cAAc,MAAgB,gCAQtC;;cAGd;IACI,CAAA;;EAIX,KAAK,OACH,QAAO,kBAAC,QAAD;GAAM,WAAU;aAAyD,OAAO,EAAM;GAAQ,CAAA;EAGvG,QACE,QAAO,OAAO,EAAM;;;AAQ1B,IAAa,IAAgC,EAAK,SAAmB,EAAE,UAAO,WAAQ,WAAQ,cAAW;AACvG,QACE,kBAAC,MAAD;EACE,WAAW;;;;UAIP,EAAO,UAAU,WAAW,gBAAgB,GAAG;UAC/C,EAAO,UAAU,UAAU,eAAe,YAAY;UACtD,IAAU,gDAAgD,GAAG;;EAExD;EACT,MAAM,IAAU,WAAW,KAAA;EAC3B,UAAU,IAAU,IAAI,KAAA;EACxB,YAAY,MAAM;AAChB,GAAI,MAAY,EAAE,QAAQ,WAAW,EAAE,QAAQ,SAC7C,EAAE,gBAAgB,EAClB,GAAS;;YAIZ,EAAY,GAAO,EAAO;EACxB,CAAA;EAEP"}
|
|
1
|
+
{"version":3,"file":"TableCell.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/table/TableCell.tsx"],"sourcesContent":["/**\n * TableCell Component\n *\n * Table cell with formatting and rendering options.\n */\n\nimport { type FC, memo, type ReactNode } from 'react';\nimport type { ColumnDefinition } from './TableHeader';\nimport {\n type ConditionalRule,\n resolveConditionalStyle,\n conditionalStyleToClassName,\n intentToCssVar,\n CONDITIONAL_ICON_PATHS,\n} from '../utils/conditionalFormat';\nimport { SparklineChart } from '../metric-card/SparklineChart';\n\n/** In-cell mini-chart kinds. */\nexport type CellChart = 'sparkline';\n\n/**\n * Extract finite numeric values from a sparkline cell value (an array of raw\n * numbers or `{value}` objects). Non-finite values (NaN/Infinity) are rejected\n * in both forms. Returns null when the value isn't a usable numeric array — so\n * the caller can decide how to render: a chart (≥2 points), a single value\n * (1 point), or fall through to the default renderer.\n */\nfunction extractSparklineValues(value: unknown): number[] | null {\n if (!Array.isArray(value) || value.length === 0) return null;\n const nums: number[] = [];\n for (const item of value) {\n if (typeof item === 'number' && Number.isFinite(item)) {\n nums.push(item);\n } else if (\n item &&\n typeof item === 'object' &&\n typeof (item as { value?: unknown }).value === 'number' &&\n Number.isFinite((item as { value: number }).value)\n ) {\n nums.push((item as { value: number }).value);\n }\n }\n return nums.length ? nums : null;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type CellFormat = 'text' | 'number' | 'currency' | 'percentage' | 'date' | 'badge' | 'link';\n\nexport interface CellFormatConfig {\n /** Format type */\n type: CellFormat;\n /** Currency code for currency format */\n currency?: string;\n /** Date format options */\n dateOptions?: Intl.DateTimeFormatOptions;\n /** Badge color mapping (value -> color) */\n badgeColors?: Record<string, string>;\n /** Link URL pattern (use {value} as placeholder) */\n linkPattern?: string;\n}\n\nexport interface TableCellProps {\n /** Cell value */\n value: unknown;\n /** Column definition */\n column: ColumnDefinition;\n /** Format configuration */\n format?: CellFormatConfig;\n /** Row data for link patterns and conditional rule field lookups */\n rowData?: Record<string, unknown>;\n /** Conditional formatting rules to evaluate against this cell */\n conditionalRules?: ConditionalRule[];\n /** Max absolute value in this column, used to scale in-cell data bars */\n dataBarMax?: number;\n /** When set, freeze this cell sticky-left at the given px offset */\n stickyLeft?: number;\n /** Render the cell as an in-cell mini chart (e.g. sparkline) instead of a value */\n cellChart?: CellChart;\n /** Click handler */\n onClick?: () => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: unknown, format?: CellFormatConfig): ReactNode {\n if (value === null || value === undefined) {\n return <span className=\"text-text-secondary\">—</span>;\n }\n\n const formatType = format?.type || 'text';\n\n switch (formatType) {\n case 'number':\n return typeof value === 'number' ? new Intl.NumberFormat('en-US').format(value) : String(value);\n\n case 'currency':\n return typeof value === 'number'\n ? new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: format?.currency || 'USD',\n }).format(value)\n : String(value);\n\n case 'percentage':\n return typeof value === 'number' ? `${(value * 100).toFixed(1)}%` : String(value);\n\n case 'date':\n if (value instanceof Date) {\n return new Intl.DateTimeFormat('en-US', format?.dateOptions || { dateStyle: 'medium' }).format(value);\n }\n if (typeof value === 'string' || typeof value === 'number') {\n const date = new Date(value);\n if (!isNaN(date.getTime())) {\n return new Intl.DateTimeFormat('en-US', format?.dateOptions || { dateStyle: 'medium' }).format(date);\n }\n }\n return String(value);\n\n case 'badge': {\n const stringValue = String(value);\n const badgeColor = format?.badgeColors?.[stringValue] || 'bg-bg-muted text-text-primary';\n return (\n <span\n className={`\n inline-flex items-center\n px-2 py-0.5\n text-xs font-medium\n rounded-full\n ${badgeColor}\n `}\n >\n {stringValue}\n </span>\n );\n }\n\n case 'link':\n return <span className=\"text-action-primary-bg hover:underline cursor-pointer\">{String(value)}</span>;\n\n case 'text':\n default:\n return String(value);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TableCell: FC<TableCellProps> = memo(function TableCell({\n value,\n column,\n format,\n rowData,\n conditionalRules,\n dataBarMax,\n stickyLeft,\n cellChart,\n onClick,\n}) {\n const cf = resolveConditionalStyle(value, conditionalRules, rowData);\n const cfClass = conditionalStyleToClassName(cf);\n const isFrozen = stickyLeft != null;\n // In-cell sparkline: only when the column opts in and the value is numeric.\n const sparkValues = cellChart === 'sparkline' ? extractSparklineValues(value) : null;\n // Range for the sparkline's accessible label (reduce, not spread → no call-stack risk).\n const sparkMin = sparkValues ? sparkValues.reduce((a, b) => Math.min(a, b), Infinity) : 0;\n const sparkMax = sparkValues ? sparkValues.reduce((a, b) => Math.max(a, b), -Infinity) : 0;\n\n // In-cell data bar width (0–100%) scaled against the column max.\n const dataBarWidth =\n cf?.dataBar && typeof value === 'number' && dataBarMax && dataBarMax > 0\n ? Math.max(0, Math.min(100, (Math.abs(value) / dataBarMax) * 100))\n : null;\n\n return (\n <td\n className={`\n relative px-4 py-3\n text-sm text-text-primary\n border-b border-border-default\n ${column.align === 'center' ? 'text-center' : ''}\n ${column.align === 'right' ? 'text-right' : 'text-left'}\n ${onClick ? 'cursor-pointer hover:text-action-primary-bg' : ''}\n ${isFrozen ? 'sticky z-10 bg-bg-surface' : ''}\n ${cfClass}\n `}\n style={isFrozen ? { left: stickyLeft, width: column.width } : undefined}\n onClick={onClick}\n role={onClick ? 'button' : undefined}\n tabIndex={onClick ? 0 : undefined}\n onKeyDown={(e) => {\n if (onClick && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n onClick();\n }\n }}\n >\n {dataBarWidth != null && (\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-y-1 left-1 rounded-sm opacity-20 pointer-events-none\"\n style={{\n width: `calc(${dataBarWidth}% - 0.5rem)`,\n backgroundColor: intentToCssVar(cf?.dataBarColor ?? 'info'),\n }}\n />\n )}\n {sparkValues && sparkValues.length >= 2 ? (\n // ≥2 points → draw the sparkline (SparklineChart needs >1 to avoid a\n // divide-by-zero). Labelled for screen readers since the SVG is decorative.\n <span\n className=\"relative inline-flex items-center\"\n role=\"img\"\n aria-label={`Sparkline, ${sparkValues.length} points, ${sparkMin.toLocaleString()} to ${sparkMax.toLocaleString()}`}\n >\n <SparklineChart data={sparkValues.map((v) => ({ value: v }))} width={96} height={28} color=\"primary\" />\n </span>\n ) : sparkValues && sparkValues.length === 1 ? (\n // A single numeric point can't be charted — render the value itself\n // (not the raw array, which would stringify to \"[object Object]\").\n <span className=\"relative inline-flex items-center\">{formatValue(sparkValues[0], format)}</span>\n ) : (\n <span className=\"relative inline-flex items-center gap-1.5\">\n {cf?.icon && (\n <svg className=\"w-3.5 h-3.5 shrink-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d={CONDITIONAL_ICON_PATHS[cf.icon]} />\n </svg>\n )}\n {formatValue(value, format)}\n </span>\n )}\n </td>\n );\n});\n\nexport default TableCell;\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { memo as e, useCallback as t } from "react";
|
|
2
2
|
import { jsx as n, jsxs as r } from "react/jsx-runtime";
|
|
3
3
|
//#region src/bigconsole/components/widgets/table/TableHeader.tsx
|
|
4
|
-
var i = e(function({ columns: e, sortState: i, onSort: a, sticky: o = !0 }) {
|
|
5
|
-
let
|
|
4
|
+
var i = e(function({ columns: e, sortState: i, onSort: a, sticky: o = !0, frozenLefts: s }) {
|
|
5
|
+
let c = t((e) => {
|
|
6
6
|
e.sortable !== !1 && a(e.key);
|
|
7
|
-
}, [a]),
|
|
7
|
+
}, [a]), l = (e) => {
|
|
8
8
|
if (e.sortable === !1) return null;
|
|
9
9
|
let t = i.column === e.key ? i.direction : null;
|
|
10
10
|
return /* @__PURE__ */ r("span", {
|
|
@@ -54,8 +54,10 @@ var i = e(function({ columns: e, sortState: i, onSort: a, sticky: o = !0 }) {
|
|
|
54
54
|
bg-bg-muted
|
|
55
55
|
${o ? "sticky top-0 z-10" : ""}
|
|
56
56
|
`,
|
|
57
|
-
children: /* @__PURE__ */ n("tr", { children: e.map((e) =>
|
|
58
|
-
|
|
57
|
+
children: /* @__PURE__ */ n("tr", { children: e.map((e) => {
|
|
58
|
+
let t = s?.[e.key];
|
|
59
|
+
return /* @__PURE__ */ n("th", {
|
|
60
|
+
className: `
|
|
59
61
|
px-4 py-3
|
|
60
62
|
text-xs font-semibold uppercase tracking-wider
|
|
61
63
|
text-text-secondary
|
|
@@ -63,20 +65,25 @@ var i = e(function({ columns: e, sortState: i, onSort: a, sticky: o = !0 }) {
|
|
|
63
65
|
${e.sortable === !1 ? "" : "cursor-pointer select-none group hover:bg-bg-surface"}
|
|
64
66
|
${e.align === "center" ? "text-center" : ""}
|
|
65
67
|
${e.align === "right" ? "text-right" : "text-left"}
|
|
68
|
+
${t == null ? "" : "sticky z-20 bg-bg-muted"}
|
|
66
69
|
`,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
e.sortable
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
style: t == null ? { width: e.width } : {
|
|
71
|
+
width: e.width,
|
|
72
|
+
left: t
|
|
73
|
+
},
|
|
74
|
+
onClick: () => c(e),
|
|
75
|
+
role: e.sortable === !1 ? void 0 : "button",
|
|
76
|
+
tabIndex: e.sortable === !1 ? void 0 : 0,
|
|
77
|
+
onKeyDown: (t) => {
|
|
78
|
+
e.sortable !== !1 && (t.key === "Enter" || t.key === " ") && (t.preventDefault(), c(e));
|
|
79
|
+
},
|
|
80
|
+
"aria-sort": i.column === e.key ? i.direction === "asc" ? "ascending" : "descending" : void 0,
|
|
81
|
+
children: /* @__PURE__ */ r("div", {
|
|
82
|
+
className: "inline-flex items-center",
|
|
83
|
+
children: [e.header, l(e)]
|
|
84
|
+
})
|
|
85
|
+
}, e.key);
|
|
86
|
+
}) })
|
|
80
87
|
});
|
|
81
88
|
});
|
|
82
89
|
//#endregion
|