@burdenoff/microfe-bigconsole 2026.812.1 → 2026.812.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.
Files changed (26) hide show
  1. package/dist/bigconsole/components/widgets/PropertiesPanel.js +480 -473
  2. package/dist/bigconsole/components/widgets/PropertiesPanel.js.map +1 -1
  3. package/dist/bigconsole/components/widgets/WidgetRegistry.js +4 -4
  4. package/dist/bigconsole/components/widgets/WidgetRegistry.js.map +1 -1
  5. package/dist/bigconsole/components/widgets/gauge/CircularGauge.js +36 -32
  6. package/dist/bigconsole/components/widgets/gauge/CircularGauge.js.map +1 -1
  7. package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js +60 -50
  8. package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js.map +1 -1
  9. package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js +63 -34
  10. package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js.map +1 -1
  11. package/dist/bigconsole/components/widgets/gauge/LinearGauge.js +46 -45
  12. package/dist/bigconsole/components/widgets/gauge/LinearGauge.js.map +1 -1
  13. package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js +152 -89
  14. package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js.map +1 -1
  15. package/dist/bigconsole/components/widgets/metric-card/MetricCardConfig.js +5 -1
  16. package/dist/bigconsole/components/widgets/metric-card/MetricCardConfig.js.map +1 -1
  17. package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +112 -89
  18. package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js.map +1 -1
  19. package/dist/bigconsole/components/widgets/metric-card/SparklineChart.js +21 -13
  20. package/dist/bigconsole/components/widgets/metric-card/SparklineChart.js.map +1 -1
  21. package/dist/bigconsole/components/widgets/metric-card/index.js +4 -4
  22. package/dist/bigconsole/components/widgets/progress/ProgressWidget.js +129 -114
  23. package/dist/bigconsole/components/widgets/progress/ProgressWidget.js.map +1 -1
  24. package/dist/bigconsole/components/widgets/utils/targetProgress.js +13 -0
  25. package/dist/bigconsole/components/widgets/utils/targetProgress.js.map +1 -0
  26. package/package.json +1 -1
@@ -3,155 +3,154 @@ import i from "./ProgressBar.js";
3
3
  import { memo as a, useCallback as o, useMemo as s } from "react";
4
4
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
5
5
  //#region src/bigconsole/components/widgets/progress/ProgressWidget.tsx
6
- var u = a(function({ widget: a, data: u, onClick: d, onDrilldown: f }) {
7
- let p = a.config, m = s(() => {
8
- if (!u) return;
9
- if (typeof u == "object" && !Array.isArray(u) && "value" in u) {
10
- let e = u.value;
11
- if (typeof e == "number") return u;
12
- if (typeof e == "string") {
13
- let t = Number(e);
14
- if (Number.isFinite(t)) return {
15
- ...u,
16
- value: t
6
+ function u(e) {
7
+ if (typeof e == "number" && Number.isFinite(e)) return e;
8
+ if (typeof e == "string") {
9
+ let t = e.trim();
10
+ if (t === "") return null;
11
+ let n = Number(t);
12
+ if (Number.isFinite(n)) return n;
13
+ }
14
+ return null;
15
+ }
16
+ function d(e, t) {
17
+ for (let n of [e, t]) if (typeof n == "number" && Number.isFinite(n) && n > 0) return n;
18
+ return 100;
19
+ }
20
+ function f(e, t) {
21
+ switch (t) {
22
+ case "currency": return new Intl.NumberFormat("en-US", {
23
+ style: "currency",
24
+ currency: "USD",
25
+ maximumFractionDigits: 0
26
+ }).format(e);
27
+ case "compact": return new Intl.NumberFormat("en-US", {
28
+ notation: "compact",
29
+ maximumFractionDigits: 1
30
+ }).format(e);
31
+ default: return new Intl.NumberFormat("en-US").format(e);
32
+ }
33
+ }
34
+ function p(e) {
35
+ let t = u(e.target);
36
+ return {
37
+ ...t === null ? {} : { target: t },
38
+ ...typeof e.description == "string" ? { description: e.description } : {}
39
+ };
40
+ }
41
+ var m = a(function({ widget: a, data: m, onClick: h, onDrilldown: g }) {
42
+ let _ = a.config, v = s(() => {
43
+ if (!m) return;
44
+ if (typeof m == "object" && !Array.isArray(m) && "value" in m) {
45
+ let e = m, t = u(e.value);
46
+ if (t !== null) {
47
+ let n = u(e.max), r = u(e.target);
48
+ return {
49
+ ...m,
50
+ value: t,
51
+ max: n ?? void 0,
52
+ target: r ?? void 0
17
53
  };
18
54
  }
19
55
  }
20
- let i = e(u);
56
+ let i = e(m);
21
57
  if (!i) return;
22
- let a = p?.valueField, o = (e, t) => t.split(".").reduce((e, t) => e && typeof e == "object" ? e[t] : void 0, e), s = (e) => {
23
- if (typeof e == "number" && Number.isFinite(e)) return e;
24
- if (typeof e == "string") {
25
- let t = Number(e);
26
- if (Number.isFinite(t)) return t;
27
- }
28
- return null;
29
- }, c = typeof i.label == "string" ? i.label : typeof i.name == "string" ? i.name : void 0;
58
+ let a = _?.valueField, o = (e, t) => t.split(".").reduce((e, t) => e && typeof e == "object" ? e[t] : void 0, e), s = typeof i.label == "string" ? i.label : typeof i.name == "string" ? i.name : void 0;
30
59
  if (a) {
31
- let e = r(u) ?? [i];
60
+ let e = r(m) ?? [i];
32
61
  for (let n of e) if (n && typeof n == "object") {
33
- let e = n, r = s(o(e, a));
62
+ let e = n, r = u(o(e, a));
34
63
  if (r !== null) {
35
- let n = typeof e.label == "string" ? e.label : typeof e.name == "string" ? e.name : c;
64
+ let n = typeof e.label == "string" ? e.label : typeof e.name == "string" ? e.name : s;
36
65
  return {
37
66
  value: r,
38
- max: p?.max ?? t(u, a) ?? void 0,
39
- label: n
67
+ max: _?.max ?? u(e.max) ?? t(m, a) ?? void 0,
68
+ label: n,
69
+ ...p(e)
40
70
  };
41
71
  }
42
72
  }
43
73
  return;
44
74
  }
45
- let l = n(i);
46
- if (!l) return;
47
- let d = s(i[l]);
48
- if (d !== null) return {
49
- value: d,
50
- max: p?.max ?? t(u, l) ?? void 0,
51
- label: c
75
+ let c = n(i);
76
+ if (!c) return;
77
+ let l = u(i[c]);
78
+ if (l !== null) return {
79
+ value: l,
80
+ max: _?.max ?? u(i.max) ?? t(m, c) ?? void 0,
81
+ label: s,
82
+ ...p(i)
52
83
  };
53
- }, [u, p]), h = s(() => {
54
- if (!m) return 0;
55
- let e = m.max || p?.max || 100;
56
- return m.value / e * 100;
57
- }, [m, p?.max]), g = s(() => {
58
- if (!m?.target) return;
59
- let e = m.max || p?.max || 100;
60
- return m.target / e * 100;
61
- }, [m, p?.max]), _ = s(() => p?.milestones?.length ? p.milestones.map((e) => ({
84
+ }, [m, _]), y = s(() => v ? v.value / d(v.max, _?.max) * 100 : 0, [v, _?.max]), b = s(() => {
85
+ if (v?.target !== void 0) return v.target / d(v.max, _?.max) * 100;
86
+ }, [v, _?.max]), x = s(() => _?.milestones?.length ? _.milestones.map((e) => ({
62
87
  position: e.position ?? e.value ?? 0,
63
88
  label: e.label,
64
- reached: h >= (e.position ?? e.value ?? 0)
65
- })) : [], [p?.milestones, h]), v = s(() => p?.colorMode === "dynamic" ? "dynamic" : p?.colorMode === "success" ? "success" : p?.colorMode === "warning" ? "warning" : p?.colorMode === "error" ? "error" : "default", [p?.colorMode]), y = s(() => {
66
- if (!m) return "0";
67
- switch (p?.format || "number") {
68
- case "currency": return new Intl.NumberFormat("en-US", {
69
- style: "currency",
70
- currency: "USD",
71
- maximumFractionDigits: 0
72
- }).format(m.value);
73
- case "compact": return new Intl.NumberFormat("en-US", {
74
- notation: "compact",
75
- maximumFractionDigits: 1
76
- }).format(m.value);
77
- default: return new Intl.NumberFormat("en-US").format(m.value);
78
- }
79
- }, [m, p?.format]), b = s(() => {
80
- let e = m?.max || p?.max || 100;
81
- switch (p?.format || "number") {
82
- case "currency": return new Intl.NumberFormat("en-US", {
83
- style: "currency",
84
- currency: "USD",
85
- maximumFractionDigits: 0
86
- }).format(e);
87
- case "compact": return new Intl.NumberFormat("en-US", {
88
- notation: "compact",
89
- maximumFractionDigits: 1
90
- }).format(e);
91
- default: return new Intl.NumberFormat("en-US").format(e);
92
- }
89
+ reached: y >= (e.position ?? e.value ?? 0)
90
+ })) : [], [_?.milestones, y]), S = s(() => _?.colorMode === "dynamic" ? "dynamic" : _?.colorMode === "success" ? "success" : _?.colorMode === "warning" ? "warning" : _?.colorMode === "error" ? "error" : "default", [_?.colorMode]), C = s(() => v ? f(v.value, _?.format || "number") : "0", [v, _?.format]), w = s(() => f(d(v?.max, _?.max), _?.format || "number"), [
91
+ v?.max,
92
+ _?.max,
93
+ _?.format
94
+ ]), T = s(() => {
95
+ if (v?.target !== void 0) return f(v.target, _?.format || "number");
96
+ }, [v?.target, _?.format]), E = o(() => {
97
+ g && v ? g({
98
+ value: v.value,
99
+ max: v.max,
100
+ target: v.target,
101
+ label: v.label,
102
+ percentage: y
103
+ }) : h && a.drilldowns?.length && h();
93
104
  }, [
94
- m?.max,
95
- p?.max,
96
- p?.format
97
- ]), x = o(() => {
98
- f && m ? f({
99
- value: m.value,
100
- max: m.max,
101
- target: m.target,
102
- label: m.label,
103
- percentage: h
104
- }) : d && a.drilldowns?.length && d();
105
- }, [
106
- d,
107
- f,
105
+ h,
106
+ g,
108
107
  a.drilldowns,
109
- m,
110
- h
111
- ]), S = !!(a.drilldown?.enabled || d && a.drilldowns?.length);
112
- return m ? /* @__PURE__ */ l("div", {
108
+ v,
109
+ y
110
+ ]), D = !!(a.drilldown?.enabled || h && a.drilldowns?.length);
111
+ return v ? /* @__PURE__ */ l("div", {
113
112
  className: `
114
113
  flex flex-col h-full p-4
115
- ${S ? "cursor-pointer hover:bg-bg-sunken/50 transition-colors" : ""}
114
+ ${D ? "cursor-pointer hover:bg-bg-sunken/50 transition-colors" : ""}
116
115
  `,
117
- onClick: x,
118
- role: S ? "button" : void 0,
119
- tabIndex: S ? 0 : void 0,
116
+ onClick: E,
117
+ role: D ? "button" : void 0,
118
+ tabIndex: D ? 0 : void 0,
120
119
  onKeyDown: (e) => {
121
- S && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), x());
120
+ D && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), E());
122
121
  },
123
122
  children: [
124
123
  /* @__PURE__ */ l("div", {
125
124
  className: "flex items-start justify-between mb-4",
126
125
  children: [/* @__PURE__ */ l("div", {
127
126
  className: "flex-1",
128
- children: [m.label && /* @__PURE__ */ c("h4", {
127
+ children: [v.label && /* @__PURE__ */ c("h4", {
129
128
  className: "text-sm font-medium text-text-primary",
130
- children: m.label
131
- }), m.description && /* @__PURE__ */ c("p", {
129
+ children: v.label
130
+ }), v.description && /* @__PURE__ */ c("p", {
132
131
  className: "text-xs text-text-secondary mt-0.5",
133
- children: m.description
132
+ children: v.description
134
133
  })]
135
134
  }), /* @__PURE__ */ l("div", {
136
135
  className: "text-right",
137
136
  children: [/* @__PURE__ */ c("span", {
138
137
  className: "text-2xl font-bold text-text-primary",
139
- children: y
140
- }), p?.showMax !== !1 && /* @__PURE__ */ l("span", {
138
+ children: C
139
+ }), _?.showMax !== !1 && /* @__PURE__ */ l("span", {
141
140
  className: "text-sm text-text-secondary",
142
- children: [" / ", b]
141
+ children: [" / ", w]
143
142
  })]
144
143
  })]
145
144
  }),
146
145
  /* @__PURE__ */ c(i, {
147
- value: h,
148
- target: g,
149
- milestones: _,
150
- height: p?.barHeight || 12,
151
- color: v,
152
- showPercentage: p?.showPercentage !== !1
146
+ value: y,
147
+ target: b,
148
+ milestones: x,
149
+ height: _?.barHeight || 12,
150
+ color: S,
151
+ showPercentage: _?.showPercentage !== !1
153
152
  }),
154
- m.target !== void 0 && /* @__PURE__ */ l("div", {
153
+ v.target !== void 0 && /* @__PURE__ */ l("div", {
155
154
  className: "mt-3 flex items-center gap-2 text-xs",
156
155
  children: [/* @__PURE__ */ c("svg", {
157
156
  className: "w-4 h-4 text-text-secondary",
@@ -161,20 +160,19 @@ var u = a(function({ widget: a, data: u, onClick: d, onDrilldown: f }) {
161
160
  }), /* @__PURE__ */ l("span", {
162
161
  className: "text-text-secondary",
163
162
  children: [
164
- "Target:",
165
- " ",
163
+ "Target: ",
166
164
  /* @__PURE__ */ c("span", {
167
165
  className: "font-medium text-text-primary",
168
- children: new Intl.NumberFormat("en-US").format(m.target)
166
+ children: T
169
167
  }),
170
- h >= (g || 0) && /* @__PURE__ */ c("span", {
168
+ y >= (b || 0) && /* @__PURE__ */ c("span", {
171
169
  className: "text-status-success-text ml-1",
172
170
  children: "Reached!"
173
171
  })
174
172
  ]
175
173
  })]
176
174
  }),
177
- S && /* @__PURE__ */ c("div", {
175
+ D && /* @__PURE__ */ c("div", {
178
176
  className: "mt-auto pt-3 flex items-center justify-end",
179
177
  children: /* @__PURE__ */ l("span", {
180
178
  className: "text-xs text-text-secondary flex items-center gap-1",
@@ -193,9 +191,26 @@ var u = a(function({ widget: a, data: u, onClick: d, onDrilldown: f }) {
193
191
  })
194
192
  })
195
193
  ]
196
- }) : null;
194
+ }) : /* @__PURE__ */ c("div", {
195
+ className: "flex items-center justify-center h-full text-text-secondary text-sm",
196
+ children: /* @__PURE__ */ l("div", {
197
+ className: "text-center",
198
+ children: [/* @__PURE__ */ c("svg", {
199
+ className: "w-12 h-12 mx-auto mb-2 text-text-tertiary",
200
+ fill: "none",
201
+ stroke: "currentColor",
202
+ viewBox: "0 0 24 24",
203
+ children: /* @__PURE__ */ c("path", {
204
+ strokeLinecap: "round",
205
+ strokeLinejoin: "round",
206
+ strokeWidth: 1.5,
207
+ d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
208
+ })
209
+ }), /* @__PURE__ */ c("p", { children: "No progress data" })]
210
+ })
211
+ });
197
212
  });
198
213
  //#endregion
199
- export { u as default };
214
+ export { m as default };
200
215
 
201
216
  //# sourceMappingURL=ProgressWidget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ProgressWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/progress/ProgressWidget.tsx"],"sourcesContent":["/**\n * ProgressWidget Component\n *\n * Progress bar widget with target, milestones, and color stages.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, ProgressConfig } from '../../../types';\nimport { ProgressBar, type Milestone } from './ProgressBar';\nimport { firstObjectRow, inferValueField, inferMaxValue, unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ProgressWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Progress data */\n data?: ProgressData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives progress data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface ProgressData {\n /** Current value */\n value: number;\n /** Maximum value (for calculating percentage) */\n max?: number;\n /** Target value */\n target?: number;\n /** Label for the progress */\n label?: string;\n /** Description text */\n description?: string;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const ProgressWidget: FC<ProgressWidgetProps> = memo(function ProgressWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as ProgressConfig | undefined;\n\n // BOFF-2246 smart default: if rawData is an array (or envelope) rather than\n // the expected `ProgressData` single object, pull the configured valueField\n // (flat OR dotted, e.g. `metrics.revenue`) or fall back to the first\n // numeric field. Auto-compute a sensible max from the dataset.\n const normalizedData = useMemo((): ProgressData | undefined => {\n if (!rawData) return undefined;\n // Already the expected shape — pass through. Accept both number and\n // numeric-string `value`, since parsers sometimes serialize metrics as\n // strings and the pre-BOFF-2246 code path rendered those via implicit\n // JS coercion in percentage math.\n if (typeof rawData === 'object' && !Array.isArray(rawData) && 'value' in rawData) {\n const rawValue = (rawData as { value: unknown }).value;\n if (typeof rawValue === 'number') {\n return rawData as ProgressData;\n }\n if (typeof rawValue === 'string') {\n const parsed = Number(rawValue);\n if (Number.isFinite(parsed)) {\n // Coerce to number so downstream math is deterministic.\n return { ...(rawData as ProgressData), value: parsed };\n }\n }\n }\n const row = firstObjectRow(rawData);\n if (!row) return undefined;\n const configuredField = (config as Record<string, unknown> | undefined)?.valueField as string | undefined;\n\n // Supports dotted paths like `metrics.revenue` so nested valueFields\n // configured in widget config resolve on array/envelope payloads the\n // same way they do on single-object payloads.\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown =>\n path.split('.').reduce<unknown>((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj);\n\n // Coerce `number` or finite numeric string to a number. Mirrors the\n // pass-through branch above so the row path doesn't silently fall back\n // to inference when a parser serializes metrics as strings.\n const toNumber = (v: unknown): number | null => {\n if (typeof v === 'number' && Number.isFinite(v)) return v;\n if (typeof v === 'string') {\n const n = Number(v);\n if (Number.isFinite(n)) return n;\n }\n return null;\n };\n\n const labelFromRow =\n typeof row.label === 'string' ? row.label : typeof row.name === 'string' ? row.name : undefined;\n\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 from\n // row[0] via inference (that would silently misrepresent user config).\n if (configuredField) {\n const rows = unwrapRows(rawData) ?? [row];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolvedRow = r as Record<string, unknown>;\n const resolved = toNumber(getNestedValue(resolvedRow, configuredField));\n if (resolved !== null) {\n const rowLabel =\n typeof resolvedRow.label === 'string'\n ? resolvedRow.label\n : typeof resolvedRow.name === 'string'\n ? resolvedRow.name\n : labelFromRow;\n return {\n value: resolved,\n max: config?.max ?? inferMaxValue(rawData, configuredField) ?? undefined,\n label: rowLabel,\n };\n }\n }\n }\n return undefined;\n }\n\n // Fall back to inference (only when no configured field). `inferValueField`\n // already filters to numeric fields, but we still coerce here for symmetry\n // with the configured branch above.\n const inferred = inferValueField(row);\n if (!inferred) return undefined;\n const inferredValue = toNumber(row[inferred]);\n if (inferredValue === null) return undefined;\n return {\n value: inferredValue,\n max: config?.max ?? inferMaxValue(rawData, inferred) ?? undefined,\n label: labelFromRow,\n };\n }, [rawData, config]);\n\n // Downstream render uses the normalized shape. Name kept as `data` so the\n // rest of the component body (formattedValue, handleClick, header, etc.)\n // stays unchanged.\n const data = normalizedData;\n\n // Calculate percentage\n const percentage = useMemo(() => {\n if (!data) return 0;\n const max = data.max || config?.max || 100;\n return (data.value / max) * 100;\n }, [data, config?.max]);\n\n // Calculate target percentage\n const targetPercentage = useMemo(() => {\n if (!data?.target) return undefined;\n const max = data.max || config?.max || 100;\n return (data.target / max) * 100;\n }, [data, config?.max]);\n\n // Build milestones from config\n const milestones: Milestone[] = useMemo(() => {\n if (!config?.milestones?.length) return [];\n return config.milestones.map((m) => ({\n position: m.position ?? m.value ?? 0,\n label: m.label,\n reached: percentage >= (m.position ?? m.value ?? 0),\n }));\n }, [config?.milestones, percentage]);\n\n // Determine color mode\n const colorMode = useMemo(() => {\n if (config?.colorMode === 'dynamic') return 'dynamic';\n if (config?.colorMode === 'success') return 'success';\n if (config?.colorMode === 'warning') return 'warning';\n if (config?.colorMode === 'error') return 'error';\n return 'default';\n }, [config?.colorMode]);\n\n // Format value\n const formattedValue = useMemo(() => {\n if (!data) return '0';\n const format = config?.format || 'number';\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(data.value);\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(data.value);\n case 'number':\n default:\n return new Intl.NumberFormat('en-US').format(data.value);\n }\n }, [data, config?.format]);\n\n const formattedMax = useMemo(() => {\n const max = data?.max || config?.max || 100;\n const format = config?.format || 'number';\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(max);\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(max);\n case 'number':\n default:\n return new Intl.NumberFormat('en-US').format(max);\n }\n }, [data?.max, config?.max, config?.format]);\n\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown && data) {\n onDrilldown({\n value: data.value,\n max: data.max,\n target: data.target,\n label: data.label,\n percentage,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, percentage]);\n\n const hasDrilldown = !!widget.drilldown?.enabled;\n const isClickable = Boolean(hasDrilldown || (onClick && widget.drilldowns?.length));\n\n if (!data) {\n return null;\n }\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/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 {/* Header */}\n <div className=\"flex items-start justify-between mb-4\">\n <div className=\"flex-1\">\n {data.label && <h4 className=\"text-sm font-medium text-text-primary\">{data.label}</h4>}\n {data.description && <p className=\"text-xs text-text-secondary mt-0.5\">{data.description}</p>}\n </div>\n\n {/* Value display */}\n <div className=\"text-right\">\n <span className=\"text-2xl font-bold text-text-primary\">{formattedValue}</span>\n {config?.showMax !== false && (\n <span className=\"text-sm text-text-secondary\">\n {' / '}\n {formattedMax}\n </span>\n )}\n </div>\n </div>\n\n {/* Progress bar */}\n <ProgressBar\n value={percentage}\n target={targetPercentage}\n milestones={milestones}\n height={config?.barHeight || 12}\n color={colorMode}\n showPercentage={config?.showPercentage !== false}\n />\n\n {/* Target info */}\n {data.target !== undefined && (\n <div className=\"mt-3 flex items-center gap-2 text-xs\">\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M5 21V3h14l-3 9 3 9H5z\" />\n </svg>\n <span className=\"text-text-secondary\">\n Target:{' '}\n <span className=\"font-medium text-text-primary\">{new Intl.NumberFormat('en-US').format(data.target)}</span>\n {percentage >= (targetPercentage || 0) && <span className=\"text-status-success-text ml-1\">Reached!</span>}\n </span>\n </div>\n )}\n\n {/* Drilldown indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-3 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 ProgressWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"ProgressWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/progress/ProgressWidget.tsx"],"sourcesContent":["/**\n * ProgressWidget Component\n *\n * Progress bar widget with target, milestones, and color stages.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, ProgressConfig } from '../../../types';\nimport { ProgressBar, type Milestone } from './ProgressBar';\nimport { firstObjectRow, inferValueField, inferMaxValue, unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ProgressWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Progress data */\n data?: ProgressData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives progress data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface ProgressData {\n /** Current value */\n value: number;\n /** Maximum value (for calculating percentage) */\n max?: number;\n /** Target value */\n target?: number;\n /** Label for the progress */\n label?: string;\n /** Description text */\n description?: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Coerce a `number` or finite numeric string to a number, else null.\n *\n * Parser output is untrusted JSON and routinely serializes metrics as strings,\n * so every numeric field (`value`, `max`, `target`) goes through this rather\n * than relying on implicit coercion or truthiness.\n */\nfunction toNumber(v: unknown): number | null {\n if (typeof v === 'number' && Number.isFinite(v)) return v;\n if (typeof v === 'string') {\n // `Number('')` and `Number(' ')` are 0, not NaN. An absent/blank field\n // must stay null — otherwise a parser emitting `{target: \"\"}` would render\n // a literal \"Target: 0\" with a marker pinned at 0%.\n const trimmed = v.trim();\n if (trimmed === '') return null;\n const n = Number(trimmed);\n if (Number.isFinite(n)) return n;\n }\n return null;\n}\n\n/**\n * Resolve the denominator for percentage maths.\n *\n * Picks the first candidate that is a usable positive number, so a max of `0`\n * (or a non-finite one) can never produce an Infinity/NaN percentage. The\n * previous `data.max || config?.max || 100` chain also treated a legitimate\n * max of 0 and a *string* max inconsistently.\n */\nfunction resolveMax(dataMax: number | undefined, configMax: number | undefined): number {\n for (const candidate of [dataMax, configMax]) {\n if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) return candidate;\n }\n return 100;\n}\n\ntype ProgressValueFormat = NonNullable<ProgressConfig['format']>;\n\n/**\n * Format a progress number according to the widget's configured format.\n *\n * Shared by value, max AND target so the three figures shown together stay\n * mutually consistent — the target used to be formatted with a bare\n * `Intl.NumberFormat`, rendering e.g. \"$4,823,500 / $5,000,000\" next to a\n * target of \"5,000,000\".\n */\nfunction formatProgressNumber(value: number, format: ProgressValueFormat): string {\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n // 'percentage' deliberately shares the plain-number branch so value, max\n // and target remain consistent (matches the pre-existing value/max behaviour).\n case 'percentage':\n case 'number':\n default:\n return new Intl.NumberFormat('en-US').format(value);\n }\n}\n\n/**\n * Optional presentation fields a DataSink row carried.\n *\n * The array/envelope branches below rebuild a fresh object, so without this\n * they silently dropped `target` and `description` — an array payload rendered\n * with no target marker, no target footer and no description while the\n * identical single-object payload rendered all three.\n */\nfunction rowExtras(row: Record<string, unknown>): Pick<ProgressData, 'target' | 'description'> {\n const target = toNumber(row.target);\n return {\n ...(target !== null ? { target } : {}),\n ...(typeof row.description === 'string' ? { description: row.description } : {}),\n };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const ProgressWidget: FC<ProgressWidgetProps> = memo(function ProgressWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as ProgressConfig | undefined;\n\n // BOFF-2246 smart default: if rawData is an array (or envelope) rather than\n // the expected `ProgressData` single object, pull the configured valueField\n // (flat OR dotted, e.g. `metrics.revenue`) or fall back to the first\n // numeric field. Auto-compute a sensible max from the dataset.\n const normalizedData = useMemo((): ProgressData | undefined => {\n if (!rawData) return undefined;\n // Already the expected shape — pass through. Accept both number and\n // numeric-string `value`, since parsers sometimes serialize metrics as\n // strings and the pre-BOFF-2246 code path rendered those via implicit\n // JS coercion in percentage math.\n if (typeof rawData === 'object' && !Array.isArray(rawData) && 'value' in rawData) {\n const source = rawData as unknown as Record<string, unknown>;\n const numericValue = toNumber(source.value);\n if (numericValue !== null) {\n // Coerce `max`/`target` with the same rule as `value`. Parser payloads\n // are untrusted JSON: a string max escapes the numeric guards below\n // (e.g. \"0\" is truthy, so `data.max || …` would select it and yield an\n // Infinity percentage), and a string target breaks the marker maths.\n const numericMax = toNumber(source.max);\n const numericTarget = toNumber(source.target);\n return {\n ...(rawData as ProgressData),\n value: numericValue,\n max: numericMax ?? undefined,\n target: numericTarget ?? undefined,\n };\n }\n }\n const row = firstObjectRow(rawData);\n if (!row) return undefined;\n const configuredField = (config as Record<string, unknown> | undefined)?.valueField as string | undefined;\n\n // Supports dotted paths like `metrics.revenue` so nested valueFields\n // configured in widget config resolve on array/envelope payloads the\n // same way they do on single-object payloads.\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown =>\n path.split('.').reduce<unknown>((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj);\n\n const labelFromRow =\n typeof row.label === 'string' ? row.label : typeof row.name === 'string' ? row.name : undefined;\n\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 from\n // row[0] via inference (that would silently misrepresent user config).\n if (configuredField) {\n const rows = unwrapRows(rawData) ?? [row];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolvedRow = r as Record<string, unknown>;\n const resolved = toNumber(getNestedValue(resolvedRow, configuredField));\n if (resolved !== null) {\n const rowLabel =\n typeof resolvedRow.label === 'string'\n ? resolvedRow.label\n : typeof resolvedRow.name === 'string'\n ? resolvedRow.name\n : labelFromRow;\n return {\n value: resolved,\n // Explicit config wins, then the row's own max (authoritative for\n // that row), then a dataset-derived guess.\n max: config?.max ?? toNumber(resolvedRow.max) ?? inferMaxValue(rawData, configuredField) ?? undefined,\n label: rowLabel,\n ...rowExtras(resolvedRow),\n };\n }\n }\n }\n return undefined;\n }\n\n // Fall back to inference (only when no configured field). `inferValueField`\n // already filters to numeric fields, but we still coerce here for symmetry\n // with the configured branch above.\n const inferred = inferValueField(row);\n if (!inferred) return undefined;\n const inferredValue = toNumber(row[inferred]);\n if (inferredValue === null) return undefined;\n return {\n value: inferredValue,\n max: config?.max ?? toNumber(row.max) ?? inferMaxValue(rawData, inferred) ?? undefined,\n label: labelFromRow,\n ...rowExtras(row),\n };\n }, [rawData, config]);\n\n // Downstream render uses the normalized shape. Name kept as `data` so the\n // rest of the component body (formattedValue, handleClick, header, etc.)\n // stays unchanged.\n const data = normalizedData;\n\n // Calculate percentage\n const percentage = useMemo(() => {\n if (!data) return 0;\n return (data.value / resolveMax(data.max, config?.max)) * 100;\n }, [data, config?.max]);\n\n // Calculate target percentage. `=== undefined` rather than a truthiness\n // check so a legitimate target of 0 still renders its marker.\n const targetPercentage = useMemo(() => {\n if (data?.target === undefined) return undefined;\n return (data.target / resolveMax(data.max, config?.max)) * 100;\n }, [data, config?.max]);\n\n // Build milestones from config\n const milestones: Milestone[] = useMemo(() => {\n if (!config?.milestones?.length) return [];\n return config.milestones.map((m) => ({\n position: m.position ?? m.value ?? 0,\n label: m.label,\n reached: percentage >= (m.position ?? m.value ?? 0),\n }));\n }, [config?.milestones, percentage]);\n\n // Determine color mode\n const colorMode = useMemo(() => {\n if (config?.colorMode === 'dynamic') return 'dynamic';\n if (config?.colorMode === 'success') return 'success';\n if (config?.colorMode === 'warning') return 'warning';\n if (config?.colorMode === 'error') return 'error';\n return 'default';\n }, [config?.colorMode]);\n\n // Format value\n const formattedValue = useMemo(() => {\n if (!data) return '0';\n return formatProgressNumber(data.value, config?.format || 'number');\n }, [data, config?.format]);\n\n const formattedMax = useMemo(() => {\n return formatProgressNumber(resolveMax(data?.max, config?.max), config?.format || 'number');\n }, [data?.max, config?.max, config?.format]);\n\n const formattedTarget = useMemo(() => {\n if (data?.target === undefined) return undefined;\n return formatProgressNumber(data.target, config?.format || 'number');\n }, [data?.target, config?.format]);\n\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown && data) {\n onDrilldown({\n value: data.value,\n max: data.max,\n target: data.target,\n label: data.label,\n percentage,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, percentage]);\n\n const hasDrilldown = !!widget.drilldown?.enabled;\n const isClickable = Boolean(hasDrilldown || (onClick && widget.drilldowns?.length));\n\n if (!data) {\n // WidgetWrapper's `isUsableData` admits payloads this widget cannot turn\n // into a number (e.g. `{\"value\":\"n/a\"}` or `[{\"name\":\"a\",\"status\":\"ok\"}]`),\n // so its own empty state never shows. Returning bare null left an\n // unexplained blank card; render the same empty state the sibling widgets\n // use instead.\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No progress data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/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 {/* Header */}\n <div className=\"flex items-start justify-between mb-4\">\n <div className=\"flex-1\">\n {data.label && <h4 className=\"text-sm font-medium text-text-primary\">{data.label}</h4>}\n {data.description && <p className=\"text-xs text-text-secondary mt-0.5\">{data.description}</p>}\n </div>\n\n {/* Value display */}\n <div className=\"text-right\">\n <span className=\"text-2xl font-bold text-text-primary\">{formattedValue}</span>\n {config?.showMax !== false && (\n <span className=\"text-sm text-text-secondary\">\n {' / '}\n {formattedMax}\n </span>\n )}\n </div>\n </div>\n\n {/* Progress bar */}\n <ProgressBar\n value={percentage}\n target={targetPercentage}\n milestones={milestones}\n height={config?.barHeight || 12}\n color={colorMode}\n showPercentage={config?.showPercentage !== false}\n />\n\n {/* Target info */}\n {data.target !== undefined && (\n <div className=\"mt-3 flex items-center gap-2 text-xs\">\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M5 21V3h14l-3 9 3 9H5z\" />\n </svg>\n <span className=\"text-text-secondary\">\n Target: <span className=\"font-medium text-text-primary\">{formattedTarget}</span>\n {percentage >= (targetPercentage || 0) && <span className=\"text-status-success-text ml-1\">Reached!</span>}\n </span>\n </div>\n )}\n\n {/* Drilldown indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-3 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 ProgressWidget;\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ //#region src/bigconsole/components/widgets/utils/targetProgress.ts
2
+ function e(e, t) {
3
+ if (typeof e != "number" || !Number.isFinite(e) || t === void 0 || t === 0 || !Number.isFinite(t)) return null;
4
+ let n = e / t * 100;
5
+ return Number.isFinite(n) ? n : null;
6
+ }
7
+ function t(e) {
8
+ return Math.min(Math.max(e, 0), 100);
9
+ }
10
+ //#endregion
11
+ export { t as clampProgressWidth, e as getTargetProgress };
12
+
13
+ //# sourceMappingURL=targetProgress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targetProgress.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/utils/targetProgress.ts"],"sourcesContent":["/**\n * Shared target-progress maths for widgets that render a \"vs target\" bar.\n *\n * Kept in one place so the KPI comparison and metric card widgets cannot drift\n * apart on the guard conditions — both previously divided by the target with no\n * protection and rendered \"NaN%\" / \"Infinity%\".\n */\n\n/**\n * Progress toward a target, as a percentage.\n *\n * Returns null whenever no meaningful ratio exists, so callers can suppress the\n * whole target block rather than render a broken figure:\n * - a non-numeric value (parser output may be a string, e.g. a status flag)\n * - an absent, zero or non-finite target (JSON payloads carry `0` and `null`)\n *\n * `Number.isFinite` does not coerce, so a runtime `null` is rejected too.\n */\nexport function getTargetProgress(value: number | string | undefined, target: number | undefined): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value)) return null;\n // Order matters: the `undefined` check narrows `target` to `number`, so no\n // cast is needed (`Number.isFinite` is not a TypeScript type guard).\n if (target === undefined || target === 0 || !Number.isFinite(target)) return null;\n const progress = (value / target) * 100;\n return Number.isFinite(progress) ? progress : null;\n}\n\n/**\n * Clamp a progress percentage to a CSS-safe bar width.\n *\n * A negative value would otherwise emit a negative width, which the browser\n * drops entirely, leaving the bar at its previous size.\n */\nexport function clampProgressWidth(progress: number): number {\n return Math.min(Math.max(progress, 0), 100);\n}\n"],"mappings":";AAkBA,SAAgB,EAAkB,GAAoC,GAA2C;AAI/G,KAHI,OAAO,KAAU,YAAY,CAAC,OAAO,SAAS,EAAM,IAGpD,MAAW,KAAA,KAAa,MAAW,KAAK,CAAC,OAAO,SAAS,EAAO,CAAE,QAAO;CAC7E,IAAM,IAAY,IAAQ,IAAU;AACpC,QAAO,OAAO,SAAS,EAAS,GAAG,IAAW;;AAShD,SAAgB,EAAmB,GAA0B;AAC3D,QAAO,KAAK,IAAI,KAAK,IAAI,GAAU,EAAE,EAAE,IAAI"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-bigconsole",
3
- "version": "2026.812.1",
3
+ "version": "2026.812.2",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [