@burdenoff/microfe-bigconsole 2026.613.2 → 2026.613.4

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 (53) hide show
  1. package/dist/bigconsole/components/dashboard/DashboardCanvas.js +142 -135
  2. package/dist/bigconsole/components/dashboard/DashboardCanvas.js.map +1 -1
  3. package/dist/bigconsole/components/dashboard/WidgetPalette.js +124 -120
  4. package/dist/bigconsole/components/dashboard/WidgetPalette.js.map +1 -1
  5. package/dist/bigconsole/components/widgets/PropertiesPanel.js +9 -9
  6. package/dist/bigconsole/components/widgets/WidgetRegistry.js +65 -8
  7. package/dist/bigconsole/components/widgets/WidgetRegistry.js.map +1 -1
  8. package/dist/bigconsole/components/widgets/WidgetTypeSelector.js +32 -27
  9. package/dist/bigconsole/components/widgets/WidgetTypeSelector.js.map +1 -1
  10. package/dist/bigconsole/components/widgets/WidgetWrapper.js +9 -9
  11. package/dist/bigconsole/components/widgets/WidgetWrapper.js.map +1 -1
  12. package/dist/bigconsole/components/widgets/chart/ChartWidget.js +123 -91
  13. package/dist/bigconsole/components/widgets/chart/ChartWidget.js.map +1 -1
  14. package/dist/bigconsole/components/widgets/chart/charts/AreaChart.js +40 -35
  15. package/dist/bigconsole/components/widgets/chart/charts/AreaChart.js.map +1 -1
  16. package/dist/bigconsole/components/widgets/chart/charts/BarChart.js +30 -25
  17. package/dist/bigconsole/components/widgets/chart/charts/BarChart.js.map +1 -1
  18. package/dist/bigconsole/components/widgets/chart/charts/ComboChart.js +139 -0
  19. package/dist/bigconsole/components/widgets/chart/charts/ComboChart.js.map +1 -0
  20. package/dist/bigconsole/components/widgets/chart/charts/LineChart.js +35 -30
  21. package/dist/bigconsole/components/widgets/chart/charts/LineChart.js.map +1 -1
  22. package/dist/bigconsole/components/widgets/chart/charts/ScatterChart.js +128 -0
  23. package/dist/bigconsole/components/widgets/chart/charts/ScatterChart.js.map +1 -0
  24. package/dist/bigconsole/components/widgets/chart/charts/WaterfallChart.js +111 -0
  25. package/dist/bigconsole/components/widgets/chart/charts/WaterfallChart.js.map +1 -0
  26. package/dist/bigconsole/components/widgets/chart/charts/index.js +4 -0
  27. package/dist/bigconsole/components/widgets/chart/charts/referenceElements.js +49 -0
  28. package/dist/bigconsole/components/widgets/chart/charts/referenceElements.js.map +1 -0
  29. package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js +72 -68
  30. package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js.map +1 -1
  31. package/dist/bigconsole/components/widgets/map-widget/MapWidget.js +74 -24
  32. package/dist/bigconsole/components/widgets/map-widget/MapWidget.js.map +1 -1
  33. package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +87 -86
  34. package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js.map +1 -1
  35. package/dist/bigconsole/components/widgets/table/TableCell.js +55 -26
  36. package/dist/bigconsole/components/widgets/table/TableCell.js.map +1 -1
  37. package/dist/bigconsole/components/widgets/table/TableHeader.js +25 -18
  38. package/dist/bigconsole/components/widgets/table/TableHeader.js.map +1 -1
  39. package/dist/bigconsole/components/widgets/table/TableWidget.js +255 -107
  40. package/dist/bigconsole/components/widgets/table/TableWidget.js.map +1 -1
  41. package/dist/bigconsole/components/widgets/utils/conditionalFormat.js +91 -0
  42. package/dist/bigconsole/components/widgets/utils/conditionalFormat.js.map +1 -0
  43. package/dist/bigconsole/hooks/index.js +2 -0
  44. package/dist/bigconsole/hooks/useDashboardImageExport.js +1 -0
  45. package/dist/bigconsole/hooks/useWidgetCatalog.js +82 -0
  46. package/dist/bigconsole/hooks/useWidgetCatalog.js.map +1 -0
  47. package/dist/bigconsole/pages/DashboardBuilderPage.js +172 -163
  48. package/dist/bigconsole/pages/DashboardBuilderPage.js.map +1 -1
  49. package/dist/bigconsole/utils/widgetTypeMapping.js +1 -0
  50. package/dist/bigconsole/utils/widgetTypeMapping.js.map +1 -1
  51. package/dist/generated/wspace-operations.js +17 -17
  52. package/dist/generated/wspace-operations.js.map +1 -1
  53. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
- import { memo as e } from "react";
2
- import { jsx as t, jsxs as n } from "react/jsx-runtime";
1
+ import { conditionalStyleToClassName as e, resolveConditionalStyle as t } from "../utils/conditionalFormat.js";
2
+ import { memo as n } from "react";
3
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
3
4
  //#region src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx
4
- function r(e, t) {
5
+ function a(e, t) {
5
6
  return t === "$" ? new Intl.NumberFormat("en-US", {
6
7
  style: "currency",
7
8
  currency: "USD",
@@ -12,21 +13,21 @@ function r(e, t) {
12
13
  maximumFractionDigits: 1
13
14
  }).format(e);
14
15
  }
15
- function i(e, t) {
16
+ function o(e, t) {
16
17
  return !e || e === "stable" ? "text-text-secondary" : e === "up" && (t ?? 0) >= 0 ? "text-state-success" : e === "down" && (t ?? 0) < 0 ? "text-state-error" : "text-text-secondary";
17
18
  }
18
- function a(e) {
19
+ function s(e) {
19
20
  return e === "up" ? "↑" : e === "down" ? "↓" : "→";
20
21
  }
21
- var o = e(function({ widget: e, data: o, onClick: s, onDrilldown: c }) {
22
- let l = !!e.drilldown?.enabled, u = e.config, d = [], f = u?.valueField || u?.value_field || "value", p = u?.labelField || u?.label_field || "label", m = (e) => Object.entries(e).filter(([, e]) => typeof e == "number").map(([e, t]) => ({
22
+ var c = n(function({ widget: n, data: c, onClick: l, onDrilldown: u }) {
23
+ let d = !!n.drilldown?.enabled, f = n.config, p = f?.conditionalRules, m = [], h = f?.valueField || f?.value_field || "value", g = f?.labelField || f?.label_field || "label", _ = (e) => Object.entries(e).filter(([, e]) => typeof e == "number").map(([e, t]) => ({
23
24
  id: e,
24
25
  label: e.replace(/([A-Z])/g, " $1").replace(/^./, (e) => e.toUpperCase()),
25
26
  value: t,
26
27
  unit: e.toLowerCase().includes("sales") || e.toLowerCase().includes("revenue") || e.toLowerCase().includes("value") ? "$" : void 0
27
- })), h = (e, t) => {
28
+ })), v = (e, t) => {
28
29
  let n = [
29
- f,
30
+ h,
30
31
  "value",
31
32
  "revenue",
32
33
  "amount",
@@ -51,7 +52,7 @@ var o = e(function({ widget: e, data: o, onClick: s, onDrilldown: c }) {
51
52
  }
52
53
  if (r === null) return null;
53
54
  let i = [
54
- p,
55
+ g,
55
56
  "label",
56
57
  "name",
57
58
  "title",
@@ -68,24 +69,24 @@ var o = e(function({ widget: e, data: o, onClick: s, onDrilldown: c }) {
68
69
  id: `kpi-${t}`,
69
70
  label: a,
70
71
  value: r,
71
- unit: f.toLowerCase().includes("revenue") || f.toLowerCase().includes("sales") ? "$" : void 0,
72
+ unit: h.toLowerCase().includes("revenue") || h.toLowerCase().includes("sales") ? "$" : void 0,
72
73
  trend: s,
73
74
  trendValue: typeof o == "number" ? o : void 0
74
75
  };
75
76
  };
76
- if (o?.kpis && Array.isArray(o.kpis)) d = o.kpis;
77
- else if (Array.isArray(o) && o.length > 0) {
78
- for (let e = 0; e < Math.min(o.length, 4); e++) {
79
- let t = o[e];
77
+ if (c?.kpis && Array.isArray(c.kpis)) m = c.kpis;
78
+ else if (Array.isArray(c) && c.length > 0) {
79
+ for (let e = 0; e < Math.min(c.length, 4); e++) {
80
+ let t = c[e];
80
81
  if (t && typeof t == "object") {
81
- let n = h(t, e);
82
- n && d.push(n);
82
+ let n = v(t, e);
83
+ n && m.push(n);
83
84
  }
84
85
  }
85
- if (d.length === 0 && o.length > 0) {
86
- let e = o[0];
87
- d = Object.entries(e).filter(([, e]) => typeof e == "number").slice(0, 4).map(([e]) => {
88
- let t = o.reduce((t, n) => {
86
+ if (m.length === 0 && c.length > 0) {
87
+ let e = c[0];
88
+ m = Object.entries(e).filter(([, e]) => typeof e == "number").slice(0, 4).map(([e]) => {
89
+ let t = c.reduce((t, n) => {
89
90
  let r = n[e];
90
91
  return t + (typeof r == "number" ? r : 0);
91
92
  }, 0);
@@ -97,92 +98,95 @@ var o = e(function({ widget: e, data: o, onClick: s, onDrilldown: c }) {
97
98
  };
98
99
  });
99
100
  }
100
- } else if (typeof o == "number" || typeof o == "string") {
101
- let t = typeof o == "string" ? parseFloat(o) : o;
102
- isNaN(t) || (d = [{
101
+ } else if (typeof c == "number" || typeof c == "string") {
102
+ let e = typeof c == "string" ? parseFloat(c) : c;
103
+ isNaN(e) || (m = [{
103
104
  id: "primary",
104
- label: (u?.subtitle || e.title || "Value").trim(),
105
- value: t,
106
- unit: u?.format === "currency" ? "$" : u?.format === "percent" ? "%" : void 0
105
+ label: (f?.subtitle || n.title || "Value").trim(),
106
+ value: e,
107
+ unit: f?.format === "currency" ? "$" : f?.format === "percent" ? "%" : void 0
107
108
  }]);
108
- } else if (o && typeof o == "object" && !Array.isArray(o)) {
109
- let e = o;
110
- "summary" in e && e.summary && typeof e.summary == "object" && (d = m(e.summary)), d.length === 0 && (d = m(e));
109
+ } else if (c && typeof c == "object" && !Array.isArray(c)) {
110
+ let e = c;
111
+ "summary" in e && e.summary && typeof e.summary == "object" && (m = _(e.summary)), m.length === 0 && (m = _(e));
111
112
  }
112
- return d.length ? /* @__PURE__ */ t("div", {
113
+ return m.length ? /* @__PURE__ */ r("div", {
113
114
  className: "h-full p-4 overflow-auto",
114
- children: /* @__PURE__ */ t("div", {
115
+ children: /* @__PURE__ */ r("div", {
115
116
  className: "grid grid-cols-2 gap-3",
116
- children: d.map((e) => /* @__PURE__ */ n("div", {
117
+ children: m.map((n) => /* @__PURE__ */ i("div", {
117
118
  className: `
118
119
  p-3 rounded-lg border border-border-default bg-bg-muted/30
119
- ${s || l ? "cursor-pointer hover:border-action-primary-bg transition-colors" : ""}
120
+ ${l || d ? "cursor-pointer hover:border-action-primary-bg transition-colors" : ""}
120
121
  `,
121
122
  onClick: () => {
122
- l && c && c({
123
- kpiId: e.id,
124
- kpiLabel: e.label,
125
- kpiValue: e.value,
126
- kpiUnit: e.unit,
127
- kpiTrend: e.trend,
128
- kpiTrendValue: e.trendValue,
129
- kpiTarget: e.target,
130
- kpiPreviousValue: e.previousValue
131
- }), s?.(e);
123
+ d && u && u({
124
+ kpiId: n.id,
125
+ kpiLabel: n.label,
126
+ kpiValue: n.value,
127
+ kpiUnit: n.unit,
128
+ kpiTrend: n.trend,
129
+ kpiTrendValue: n.trendValue,
130
+ kpiTarget: n.target,
131
+ kpiPreviousValue: n.previousValue
132
+ }), l?.(n);
132
133
  },
133
134
  children: [
134
- /* @__PURE__ */ t("p", {
135
+ /* @__PURE__ */ r("p", {
135
136
  className: "text-xs text-text-secondary truncate mb-1",
136
- children: e.label
137
+ children: n.label
137
138
  }),
138
- /* @__PURE__ */ t("p", {
139
- className: "text-xl font-semibold text-text-primary",
140
- children: r(e.value, e.unit)
139
+ /* @__PURE__ */ r("p", {
140
+ className: `text-xl font-semibold ${e(t(n.value, p, {
141
+ [n.label]: n.value,
142
+ value: n.value
143
+ })) || "text-text-primary"}`,
144
+ children: a(n.value, n.unit)
141
145
  }),
142
- e.trendValue !== void 0 && /* @__PURE__ */ n("div", {
143
- className: `flex items-center gap-1 mt-1 text-xs ${i(e.trend, e.trendValue)}`,
144
- children: [/* @__PURE__ */ t("span", { children: a(e.trend) }), /* @__PURE__ */ n("span", { children: [
145
- e.trendValue >= 0 ? "+" : "",
146
- e.trendValue.toFixed(1),
146
+ n.trendValue !== void 0 && /* @__PURE__ */ i("div", {
147
+ className: `flex items-center gap-1 mt-1 text-xs ${o(n.trend, n.trendValue)}`,
148
+ children: [/* @__PURE__ */ r("span", { children: s(n.trend) }), /* @__PURE__ */ i("span", { children: [
149
+ n.trendValue >= 0 ? "+" : "",
150
+ n.trendValue.toFixed(1),
147
151
  "%"
148
152
  ] })]
149
153
  }),
150
- e.target !== void 0 && /* @__PURE__ */ n("div", {
154
+ n.target !== void 0 && /* @__PURE__ */ i("div", {
151
155
  className: "mt-2",
152
- children: [/* @__PURE__ */ n("div", {
156
+ children: [/* @__PURE__ */ i("div", {
153
157
  className: "flex justify-between text-[10px] text-text-tertiary mb-0.5",
154
- children: [/* @__PURE__ */ t("span", { children: "vs target" }), /* @__PURE__ */ n("span", { children: [(e.value / e.target * 100).toFixed(0), "%"] })]
155
- }), /* @__PURE__ */ t("div", {
158
+ children: [/* @__PURE__ */ r("span", { children: "vs target" }), /* @__PURE__ */ i("span", { children: [(n.value / n.target * 100).toFixed(0), "%"] })]
159
+ }), /* @__PURE__ */ r("div", {
156
160
  className: "h-1 bg-bg-muted rounded-full overflow-hidden",
157
- children: /* @__PURE__ */ t("div", {
158
- className: `h-full rounded-full ${e.value >= e.target ? "bg-state-success" : "bg-action-primary-bg"}`,
159
- style: { width: `${Math.min(e.value / e.target * 100, 100)}%` }
161
+ children: /* @__PURE__ */ r("div", {
162
+ className: `h-full rounded-full ${n.value >= n.target ? "bg-state-success" : "bg-action-primary-bg"}`,
163
+ style: { width: `${Math.min(n.value / n.target * 100, 100)}%` }
160
164
  })
161
165
  })]
162
166
  })
163
167
  ]
164
- }, e.id))
168
+ }, n.id))
165
169
  })
166
- }) : /* @__PURE__ */ t("div", {
170
+ }) : /* @__PURE__ */ r("div", {
167
171
  className: "flex items-center justify-center h-full text-text-secondary text-sm",
168
- children: /* @__PURE__ */ n("div", {
172
+ children: /* @__PURE__ */ i("div", {
169
173
  className: "text-center",
170
- children: [/* @__PURE__ */ t("svg", {
174
+ children: [/* @__PURE__ */ r("svg", {
171
175
  className: "w-12 h-12 mx-auto mb-2 text-text-tertiary",
172
176
  fill: "none",
173
177
  stroke: "currentColor",
174
178
  viewBox: "0 0 24 24",
175
- children: /* @__PURE__ */ t("path", {
179
+ children: /* @__PURE__ */ r("path", {
176
180
  strokeLinecap: "round",
177
181
  strokeLinejoin: "round",
178
182
  strokeWidth: 1.5,
179
183
  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"
180
184
  })
181
- }), /* @__PURE__ */ t("p", { children: "No KPI data" })]
185
+ }), /* @__PURE__ */ r("p", { children: "No KPI data" })]
182
186
  })
183
187
  });
184
188
  });
185
189
  //#endregion
186
- export { o as default };
190
+ export { c as default };
187
191
 
188
192
  //# sourceMappingURL=KPIComparisonWidget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"KPIComparisonWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx"],"sourcesContent":["/**\n * KPIComparisonWidget Component\n *\n * Displays multiple KPIs in a grid for comparison.\n */\n\nimport { type FC, memo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIComparisonWidgetProps {\n widget: Widget;\n data?: KPIComparisonData;\n onClick?: (kpi: KPIItem) => void;\n onDrilldown?: (data: Record<string, unknown>) => void;\n}\n\nexport interface KPIItem {\n id: string;\n label: string;\n value: number;\n previousValue?: number;\n target?: number;\n unit?: string;\n trend?: 'up' | 'down' | 'stable';\n trendValue?: number;\n}\n\nexport interface KPIComparisonData {\n kpis?: KPIItem[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number, unit?: string): string {\n if (unit === '$') {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n }\n if (unit === '%') {\n return `${value.toFixed(1)}%`;\n }\n return new Intl.NumberFormat('en-US', {\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction getTrendColor(trend?: string, trendValue?: number): string {\n if (!trend || trend === 'stable') return 'text-text-secondary';\n if (trend === 'up' && (trendValue ?? 0) >= 0) return 'text-state-success';\n if (trend === 'down' && (trendValue ?? 0) < 0) return 'text-state-error';\n return 'text-text-secondary';\n}\n\nfunction getTrendIcon(trend?: string): string {\n if (trend === 'up') return '↑';\n if (trend === 'down') return '↓';\n return '→';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIComparisonWidget: FC<KPIComparisonWidgetProps> = memo(function KPIComparisonWidget({\n widget,\n data,\n onClick,\n onDrilldown,\n}) {\n const hasDrilldown = !!widget.drilldown?.enabled;\n const config = widget.config as Record<string, unknown> | undefined;\n\n // Build KPIs array from data\n // Support multiple formats:\n // 1. data.kpis array (standard format)\n // 2. Single value from data.processed (e.g., 125000 from DataSink)\n // 3. data is a number/string directly\n // 4. Nested summary object (e.g., {sales: [...], summary: {totalSales: 123}})\n // 5. Array of objects from DataSink (e.g., [{month: \"Jan\", revenue: 45000}, ...])\n let kpis: KPIItem[] = [];\n\n // Get configured value field from widget config (for DataSink data)\n const valueField = (config?.valueField as string) || (config?.value_field as string) || 'value';\n const labelField = (config?.labelField as string) || (config?.label_field as string) || 'label';\n\n // Helper to convert object with numeric values to KPIs\n const objectToKpis = (obj: Record<string, unknown>): KPIItem[] => {\n const entries = Object.entries(obj);\n return entries\n .filter(([, v]) => typeof v === 'number')\n .map(([key, value]) => ({\n id: key,\n label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: value as number,\n unit:\n key.toLowerCase().includes('sales') ||\n key.toLowerCase().includes('revenue') ||\n key.toLowerCase().includes('value')\n ? '$'\n : undefined,\n }));\n };\n\n // Helper to extract KPI from a single data row using config fields\n const extractKpiFromRow = (row: Record<string, unknown>, index: number): KPIItem | null => {\n // Try to get value from configured field or common field names\n const valueCandidates = [valueField, 'value', 'revenue', 'amount', 'total', 'count', 'sales', 'growth'];\n let value: number | null = null;\n\n for (const field of valueCandidates) {\n if (typeof row[field] === 'number') {\n value = row[field] as number;\n break;\n }\n if (typeof row[field] === 'string' && !isNaN(parseFloat(row[field] as string))) {\n value = parseFloat(row[field] as string);\n break;\n }\n }\n\n if (value === null) {\n // Try to find any numeric field\n const numericEntry = Object.entries(row).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n value = numericEntry[1] as number;\n }\n }\n\n if (value === null) return null;\n\n // Try to get label from configured field or common field names\n const labelCandidates = [labelField, 'label', 'name', 'title', 'month', 'region', 'category'];\n let label = `Item ${index + 1}`;\n\n for (const field of labelCandidates) {\n if (row[field] && typeof row[field] === 'string') {\n label = row[field] as string;\n break;\n }\n }\n\n // Check for growth/trend fields\n const growthValue = row.growth ?? row.change ?? row.trend ?? row.percentChange;\n const trend =\n typeof growthValue === 'number' ? (growthValue > 0 ? 'up' : growthValue < 0 ? 'down' : 'stable') : undefined;\n\n return {\n id: `kpi-${index}`,\n label,\n value,\n unit:\n valueField.toLowerCase().includes('revenue') || valueField.toLowerCase().includes('sales') ? '$' : undefined,\n trend: trend as 'up' | 'down' | 'stable' | undefined,\n trendValue: typeof growthValue === 'number' ? growthValue : undefined,\n };\n };\n\n if (data?.kpis && Array.isArray(data.kpis)) {\n // Standard KPIs array\n kpis = data.kpis;\n } else if (Array.isArray(data) && data.length > 0) {\n // NEW: Handle array of objects from DataSink\n // Extract KPIs from each row, up to 4 items for display\n const maxKpis = 4;\n for (let i = 0; i < Math.min(data.length, maxKpis); i++) {\n const row = data[i] as Record<string, unknown>;\n if (row && typeof row === 'object') {\n const kpi = extractKpiFromRow(row, i);\n if (kpi) kpis.push(kpi);\n }\n }\n\n // If we couldn't extract KPIs from rows, try to aggregate numeric fields\n if (kpis.length === 0 && data.length > 0) {\n const firstRow = data[0] as Record<string, unknown>;\n const numericFields = Object.entries(firstRow).filter(([, v]) => typeof v === 'number');\n\n // Create KPIs by aggregating (summing) each numeric field across all rows\n kpis = numericFields.slice(0, maxKpis).map(([field]) => {\n const sum = data.reduce((acc, row) => {\n const val = (row as Record<string, unknown>)[field];\n return acc + (typeof val === 'number' ? val : 0);\n }, 0);\n\n return {\n id: field,\n label: field.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: sum,\n unit: field.toLowerCase().includes('revenue') || field.toLowerCase().includes('sales') ? '$' : undefined,\n };\n });\n }\n } else if (typeof data === 'number' || typeof data === 'string') {\n // Single value - create a KPI from it using widget config\n const value = typeof data === 'string' ? parseFloat(data) : data;\n if (!isNaN(value)) {\n kpis = [\n {\n id: 'primary',\n label: ((config?.subtitle as string) || widget.title || 'Value').trim(),\n value,\n unit: config?.format === 'currency' ? '$' : config?.format === 'percent' ? '%' : undefined,\n },\n ];\n }\n } else if (data && typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as Record<string, unknown>;\n\n // First, check if there's a nested 'summary' object with numeric values\n if ('summary' in dataObj && dataObj.summary && typeof dataObj.summary === 'object') {\n kpis = objectToKpis(dataObj.summary as Record<string, unknown>);\n }\n\n // If no KPIs from summary, try top-level numeric values\n if (kpis.length === 0) {\n kpis = objectToKpis(dataObj);\n }\n }\n\n if (!kpis.length) {\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 KPI data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full p-4 overflow-auto\">\n <div className=\"grid grid-cols-2 gap-3\">\n {kpis.map((kpi) => (\n <div\n key={kpi.id}\n className={`\n p-3 rounded-lg border border-border-default bg-bg-muted/30\n ${onClick || hasDrilldown ? 'cursor-pointer hover:border-action-primary-bg transition-colors' : ''}\n `}\n onClick={() => {\n // Trigger drilldown with KPI data if enabled\n if (hasDrilldown && onDrilldown) {\n onDrilldown({\n kpiId: kpi.id,\n kpiLabel: kpi.label,\n kpiValue: kpi.value,\n kpiUnit: kpi.unit,\n kpiTrend: kpi.trend,\n kpiTrendValue: kpi.trendValue,\n kpiTarget: kpi.target,\n kpiPreviousValue: kpi.previousValue,\n });\n }\n onClick?.(kpi);\n }}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary truncate mb-1\">{kpi.label}</p>\n\n {/* Value */}\n <p className=\"text-xl font-semibold text-text-primary\">{formatValue(kpi.value, kpi.unit)}</p>\n\n {/* Trend */}\n {kpi.trendValue !== undefined && (\n <div className={`flex items-center gap-1 mt-1 text-xs ${getTrendColor(kpi.trend, kpi.trendValue)}`}>\n <span>{getTrendIcon(kpi.trend)}</span>\n <span>\n {kpi.trendValue >= 0 ? '+' : ''}\n {kpi.trendValue.toFixed(1)}%\n </span>\n </div>\n )}\n\n {/* Target progress */}\n {kpi.target !== undefined && (\n <div className=\"mt-2\">\n <div className=\"flex justify-between text-[10px] text-text-tertiary mb-0.5\">\n <span>vs target</span>\n <span>{((kpi.value / kpi.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1 bg-bg-muted rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${kpi.value >= kpi.target ? 'bg-state-success' : 'bg-action-primary-bg'}`}\n style={{ width: `${Math.min((kpi.value / kpi.target) * 100, 100)}%` }}\n />\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIComparisonWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"KPIComparisonWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx"],"sourcesContent":["/**\n * KPIComparisonWidget Component\n *\n * Displays multiple KPIs in a grid for comparison.\n */\n\nimport { type FC, memo } from 'react';\nimport type { Widget } from '../../../types';\nimport { type ConditionalRule, resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIComparisonWidgetProps {\n widget: Widget;\n data?: KPIComparisonData;\n onClick?: (kpi: KPIItem) => void;\n onDrilldown?: (data: Record<string, unknown>) => void;\n}\n\nexport interface KPIItem {\n id: string;\n label: string;\n value: number;\n previousValue?: number;\n target?: number;\n unit?: string;\n trend?: 'up' | 'down' | 'stable';\n trendValue?: number;\n}\n\nexport interface KPIComparisonData {\n kpis?: KPIItem[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number, unit?: string): string {\n if (unit === '$') {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n }\n if (unit === '%') {\n return `${value.toFixed(1)}%`;\n }\n return new Intl.NumberFormat('en-US', {\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction getTrendColor(trend?: string, trendValue?: number): string {\n if (!trend || trend === 'stable') return 'text-text-secondary';\n if (trend === 'up' && (trendValue ?? 0) >= 0) return 'text-state-success';\n if (trend === 'down' && (trendValue ?? 0) < 0) return 'text-state-error';\n return 'text-text-secondary';\n}\n\nfunction getTrendIcon(trend?: string): string {\n if (trend === 'up') return '↑';\n if (trend === 'down') return '↓';\n return '→';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIComparisonWidget: FC<KPIComparisonWidgetProps> = memo(function KPIComparisonWidget({\n widget,\n data,\n onClick,\n onDrilldown,\n}) {\n const hasDrilldown = !!widget.drilldown?.enabled;\n const config = widget.config as Record<string, unknown> | undefined;\n const conditionalRules = config?.conditionalRules as ConditionalRule[] | undefined;\n\n // Build KPIs array from data\n // Support multiple formats:\n // 1. data.kpis array (standard format)\n // 2. Single value from data.processed (e.g., 125000 from DataSink)\n // 3. data is a number/string directly\n // 4. Nested summary object (e.g., {sales: [...], summary: {totalSales: 123}})\n // 5. Array of objects from DataSink (e.g., [{month: \"Jan\", revenue: 45000}, ...])\n let kpis: KPIItem[] = [];\n\n // Get configured value field from widget config (for DataSink data)\n const valueField = (config?.valueField as string) || (config?.value_field as string) || 'value';\n const labelField = (config?.labelField as string) || (config?.label_field as string) || 'label';\n\n // Helper to convert object with numeric values to KPIs\n const objectToKpis = (obj: Record<string, unknown>): KPIItem[] => {\n const entries = Object.entries(obj);\n return entries\n .filter(([, v]) => typeof v === 'number')\n .map(([key, value]) => ({\n id: key,\n label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: value as number,\n unit:\n key.toLowerCase().includes('sales') ||\n key.toLowerCase().includes('revenue') ||\n key.toLowerCase().includes('value')\n ? '$'\n : undefined,\n }));\n };\n\n // Helper to extract KPI from a single data row using config fields\n const extractKpiFromRow = (row: Record<string, unknown>, index: number): KPIItem | null => {\n // Try to get value from configured field or common field names\n const valueCandidates = [valueField, 'value', 'revenue', 'amount', 'total', 'count', 'sales', 'growth'];\n let value: number | null = null;\n\n for (const field of valueCandidates) {\n if (typeof row[field] === 'number') {\n value = row[field] as number;\n break;\n }\n if (typeof row[field] === 'string' && !isNaN(parseFloat(row[field] as string))) {\n value = parseFloat(row[field] as string);\n break;\n }\n }\n\n if (value === null) {\n // Try to find any numeric field\n const numericEntry = Object.entries(row).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n value = numericEntry[1] as number;\n }\n }\n\n if (value === null) return null;\n\n // Try to get label from configured field or common field names\n const labelCandidates = [labelField, 'label', 'name', 'title', 'month', 'region', 'category'];\n let label = `Item ${index + 1}`;\n\n for (const field of labelCandidates) {\n if (row[field] && typeof row[field] === 'string') {\n label = row[field] as string;\n break;\n }\n }\n\n // Check for growth/trend fields\n const growthValue = row.growth ?? row.change ?? row.trend ?? row.percentChange;\n const trend =\n typeof growthValue === 'number' ? (growthValue > 0 ? 'up' : growthValue < 0 ? 'down' : 'stable') : undefined;\n\n return {\n id: `kpi-${index}`,\n label,\n value,\n unit:\n valueField.toLowerCase().includes('revenue') || valueField.toLowerCase().includes('sales') ? '$' : undefined,\n trend: trend as 'up' | 'down' | 'stable' | undefined,\n trendValue: typeof growthValue === 'number' ? growthValue : undefined,\n };\n };\n\n if (data?.kpis && Array.isArray(data.kpis)) {\n // Standard KPIs array\n kpis = data.kpis;\n } else if (Array.isArray(data) && data.length > 0) {\n // NEW: Handle array of objects from DataSink\n // Extract KPIs from each row, up to 4 items for display\n const maxKpis = 4;\n for (let i = 0; i < Math.min(data.length, maxKpis); i++) {\n const row = data[i] as Record<string, unknown>;\n if (row && typeof row === 'object') {\n const kpi = extractKpiFromRow(row, i);\n if (kpi) kpis.push(kpi);\n }\n }\n\n // If we couldn't extract KPIs from rows, try to aggregate numeric fields\n if (kpis.length === 0 && data.length > 0) {\n const firstRow = data[0] as Record<string, unknown>;\n const numericFields = Object.entries(firstRow).filter(([, v]) => typeof v === 'number');\n\n // Create KPIs by aggregating (summing) each numeric field across all rows\n kpis = numericFields.slice(0, maxKpis).map(([field]) => {\n const sum = data.reduce((acc, row) => {\n const val = (row as Record<string, unknown>)[field];\n return acc + (typeof val === 'number' ? val : 0);\n }, 0);\n\n return {\n id: field,\n label: field.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: sum,\n unit: field.toLowerCase().includes('revenue') || field.toLowerCase().includes('sales') ? '$' : undefined,\n };\n });\n }\n } else if (typeof data === 'number' || typeof data === 'string') {\n // Single value - create a KPI from it using widget config\n const value = typeof data === 'string' ? parseFloat(data) : data;\n if (!isNaN(value)) {\n kpis = [\n {\n id: 'primary',\n label: ((config?.subtitle as string) || widget.title || 'Value').trim(),\n value,\n unit: config?.format === 'currency' ? '$' : config?.format === 'percent' ? '%' : undefined,\n },\n ];\n }\n } else if (data && typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as Record<string, unknown>;\n\n // First, check if there's a nested 'summary' object with numeric values\n if ('summary' in dataObj && dataObj.summary && typeof dataObj.summary === 'object') {\n kpis = objectToKpis(dataObj.summary as Record<string, unknown>);\n }\n\n // If no KPIs from summary, try top-level numeric values\n if (kpis.length === 0) {\n kpis = objectToKpis(dataObj);\n }\n }\n\n if (!kpis.length) {\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 KPI data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full p-4 overflow-auto\">\n <div className=\"grid grid-cols-2 gap-3\">\n {kpis.map((kpi) => (\n <div\n key={kpi.id}\n className={`\n p-3 rounded-lg border border-border-default bg-bg-muted/30\n ${onClick || hasDrilldown ? 'cursor-pointer hover:border-action-primary-bg transition-colors' : ''}\n `}\n onClick={() => {\n // Trigger drilldown with KPI data if enabled\n if (hasDrilldown && onDrilldown) {\n onDrilldown({\n kpiId: kpi.id,\n kpiLabel: kpi.label,\n kpiValue: kpi.value,\n kpiUnit: kpi.unit,\n kpiTrend: kpi.trend,\n kpiTrendValue: kpi.trendValue,\n kpiTarget: kpi.target,\n kpiPreviousValue: kpi.previousValue,\n });\n }\n onClick?.(kpi);\n }}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary truncate mb-1\">{kpi.label}</p>\n\n {/* Value (with optional threshold-based conditional color) */}\n <p\n className={`text-xl font-semibold ${\n conditionalStyleToClassName(\n resolveConditionalStyle(kpi.value, conditionalRules, { [kpi.label]: kpi.value, value: kpi.value })\n ) || 'text-text-primary'\n }`}\n >\n {formatValue(kpi.value, kpi.unit)}\n </p>\n\n {/* Trend */}\n {kpi.trendValue !== undefined && (\n <div className={`flex items-center gap-1 mt-1 text-xs ${getTrendColor(kpi.trend, kpi.trendValue)}`}>\n <span>{getTrendIcon(kpi.trend)}</span>\n <span>\n {kpi.trendValue >= 0 ? '+' : ''}\n {kpi.trendValue.toFixed(1)}%\n </span>\n </div>\n )}\n\n {/* Target progress */}\n {kpi.target !== undefined && (\n <div className=\"mt-2\">\n <div className=\"flex justify-between text-[10px] text-text-tertiary mb-0.5\">\n <span>vs target</span>\n <span>{((kpi.value / kpi.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1 bg-bg-muted rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${kpi.value >= kpi.target ? 'bg-state-success' : 'bg-action-primary-bg'}`}\n style={{ width: `${Math.min((kpi.value / kpi.target) * 100, 100)}%` }}\n />\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIComparisonWidget;\n"],"mappings":"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"}
@@ -28,19 +28,29 @@ function h(e) {
28
28
  } : void 0;
29
29
  }
30
30
  function g(e, t) {
31
- let n = Array.isArray(e) ? void 0 : e, r = Array.isArray(e) ? e : e?.markers;
31
+ let n = Array.isArray(e) ? void 0 : e, r = Array.isArray(e) ? e : e?.markers, i = Array.isArray(r) ? r.filter((e) => e && m(e.lat) && m(e.lng)) : [], a = (n?.geoJson ?? t.geoJson) || void 0, o = t.regionIdField || "id", s = /* @__PURE__ */ new Map(), c = n?.regions ?? t.regions;
32
+ if (Array.isArray(c)) for (let e of c) {
33
+ if (!e) continue;
34
+ let t = e[o] ?? e.id ?? e.region ?? e.code, n = e.value ?? e.count ?? e.total;
35
+ t != null && m(n) && s.set(String(t), {
36
+ value: n,
37
+ name: typeof e.name == "string" ? e.name : void 0
38
+ });
39
+ }
32
40
  return {
33
- markers: Array.isArray(r) ? r.filter((e) => e && m(e.lat) && m(e.lng)) : [],
41
+ markers: i,
34
42
  center: h(n?.center),
35
43
  zoom: m(n?.zoom) ? n?.zoom : void 0,
36
44
  fallbackCenter: h(t.center),
37
- fallbackZoom: m(t.zoom) ? t.zoom : void 0
45
+ fallbackZoom: m(t.zoom) ? t.zoom : void 0,
46
+ geoJson: a && Array.isArray(a.features) ? a : void 0,
47
+ regionValues: s
38
48
  };
39
49
  }
40
50
  var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
41
- let f = r(null), h = r(null), _ = r(null), [v, y] = i("loading"), b = n(() => e?.config ?? {}, [e?.config]), x = b.showMarkers !== !1, { markers: S, center: C, zoom: w, fallbackCenter: T, fallbackZoom: E } = n(() => g(c, b), [c, b]), D = r(() => void 0);
51
+ let f = r(null), h = r(null), _ = r(null), [v, y] = i("loading"), b = n(() => e?.config ?? {}, [e?.config]), x = b.showMarkers !== !1, S = b.regionIdField || "id", { markers: C, center: w, zoom: T, fallbackCenter: E, fallbackZoom: D, geoJson: O, regionValues: k } = n(() => g(c, b), [c, b]), A = !!(O && k.size > 0), j = r(() => void 0);
42
52
  return t(() => {
43
- D.current = (e) => {
53
+ j.current = (e) => {
44
54
  u ? u(e) : l && l(e);
45
55
  };
46
56
  }, [l, u]), t(() => {
@@ -77,8 +87,38 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
77
87
  }, []), t(() => {
78
88
  let e = h.current;
79
89
  if (!e) return;
80
- let t = s.layerGroup();
81
- if (x) for (let e of S) {
90
+ let t = s.layerGroup(), n = null;
91
+ if (O && k.size > 0) {
92
+ let e = Array.from(k.values()).map((e) => e.value), r = Math.min(...e), i = Math.max(...e) - r || 1, a = (e) => {
93
+ let t = e.properties ?? {}, n = e.id ?? t[S] ?? t.id ?? t.code;
94
+ return n == null ? void 0 : k.get(String(n));
95
+ };
96
+ n = s.geoJSON(O, {
97
+ style: (e) => {
98
+ let t = e ? a(e) : void 0, n = t ? (t.value - r) / i : 0;
99
+ return {
100
+ fillColor: "var(--color-status-info, #3b82f6)",
101
+ fillOpacity: t ? .2 + n * .65 : .05,
102
+ color: "var(--color-border-default, #cbd5e1)",
103
+ weight: 1
104
+ };
105
+ },
106
+ onEachFeature: (e, t) => {
107
+ let n = a(e), r = n?.name ?? (typeof e.properties?.name == "string" ? e.properties.name : void 0) ?? String(e.id ?? ""), i = document.createElement("div"), o = document.createElement("div");
108
+ if (o.style.fontWeight = "600", o.textContent = r || "Region", i.appendChild(o), n) {
109
+ let e = document.createElement("div");
110
+ e.textContent = n.value.toLocaleString(), i.appendChild(e);
111
+ }
112
+ t.bindPopup(i), t.on("click", () => j.current({
113
+ name: r,
114
+ lat: NaN,
115
+ lng: NaN,
116
+ value: n?.value
117
+ }));
118
+ }
119
+ }), n.addTo(t);
120
+ }
121
+ if (x) for (let e of C) {
82
122
  let n = document.createElement("div");
83
123
  Object.assign(n.style, {
84
124
  width: "14px",
@@ -99,36 +139,46 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
99
139
  let t = document.createElement("div");
100
140
  t.textContent = e.value.toLocaleString(), a.appendChild(t);
101
141
  }
102
- i.bindPopup(a), i.on("click", () => D.current(e));
142
+ i.bindPopup(a), i.on("click", () => j.current(e));
103
143
  }
104
144
  t.addTo(e);
105
- let n = JSON.stringify({
106
- m: S.map((e) => [e.lat, e.lng]),
107
- c: C ? [C.lat, C.lng] : null,
108
- z: w ?? null,
109
- fc: T ? [T.lat, T.lng] : null,
110
- fz: E ?? null
145
+ let r = JSON.stringify({
146
+ m: C.map((e) => [e.lat, e.lng]),
147
+ c: w ? [w.lat, w.lng] : null,
148
+ z: T ?? null,
149
+ fc: E ? [E.lat, E.lng] : null,
150
+ fz: D ?? null,
151
+ g: O ? k.size : 0
111
152
  });
112
- if (n !== _.current) if (_.current = n, C) e.setView([C.lat, C.lng], w ?? e.getZoom());
113
- else if (S.length === 1) e.setView([S[0].lat, S[0].lng], w ?? 8);
114
- else if (S.length > 1) {
115
- let t = s.latLngBounds(S.map((e) => [e.lat, e.lng]));
153
+ if (r !== _.current) if (_.current = r, w) e.setView([w.lat, w.lng], T ?? e.getZoom());
154
+ else if (!C.length && n) {
155
+ let t = n.getBounds();
156
+ t.isValid() && e.fitBounds(t, {
157
+ padding: [24, 24],
158
+ maxZoom: 12
159
+ });
160
+ } else if (C.length === 1) e.setView([C[0].lat, C[0].lng], T ?? 8);
161
+ else if (C.length > 1) {
162
+ let t = s.latLngBounds(C.map((e) => [e.lat, e.lng]));
116
163
  e.fitBounds(t, {
117
164
  padding: [32, 32],
118
165
  maxZoom: 12
119
166
  });
120
- } else T ? e.setView([T.lat, T.lng], w ?? E ?? e.getZoom()) : m(w ?? E) && e.setZoom(w ?? E);
121
- let r = window.setTimeout(() => e.invalidateSize(), 0);
167
+ } else E ? e.setView([E.lat, E.lng], T ?? D ?? e.getZoom()) : m(T ?? D) && e.setZoom(T ?? D);
168
+ let i = window.setTimeout(() => e.invalidateSize(), 0);
122
169
  return () => {
123
- window.clearTimeout(r), t.remove();
170
+ window.clearTimeout(i), t.remove();
124
171
  };
125
172
  }, [
126
- S,
127
173
  C,
128
174
  w,
129
175
  T,
130
176
  E,
131
- x
177
+ D,
178
+ x,
179
+ O,
180
+ k,
181
+ S
132
182
  ]), /* @__PURE__ */ o("div", {
133
183
  className: "relative h-full w-full overflow-hidden rounded-lg bg-bg-muted",
134
184
  children: [
@@ -136,7 +186,7 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
136
186
  ref: f,
137
187
  className: "h-full w-full"
138
188
  }),
139
- v !== "error" && S.length === 0 && /* @__PURE__ */ a("div", {
189
+ v !== "error" && C.length === 0 && !A && /* @__PURE__ */ a("div", {
140
190
  className: "pointer-events-none absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted/70 text-sm text-text-secondary",
141
191
  children: /* @__PURE__ */ o("div", {
142
192
  className: "text-center",
@@ -1 +1 @@
1
- {"version":3,"file":"MapWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/map-widget/MapWidget.tsx"],"sourcesContent":["/**\n * MapWidget Component\n *\n * Interactive geographic map (Leaflet + OpenStreetMap tiles) with data-bound\n * markers. Leaflet's stylesheet is loaded from a CDN at runtime (with SRI) because\n * the build-time MFE library bundle does not ship a component's CSS to the host\n * shell — consistent with the map being an inherently online feature (tiles).\n */\n\nimport { type FC, memo, useEffect, useMemo, useRef, useState } from 'react';\nimport L from 'leaflet';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MapWidgetProps {\n widget: Widget;\n data?: MapData;\n onClick?: (marker: MapMarker) => void;\n /** Drilldown handler - receives selected marker data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MapMarker {\n name: string;\n lat: number;\n lng: number;\n value?: number;\n popup?: string;\n color?: string;\n}\n\nexport interface MapData {\n center?: { lat: number; lng: number };\n zoom?: number;\n markers?: MapMarker[];\n}\n\n// ============================================================================\n// Leaflet CSS (runtime CDN injection)\n// ============================================================================\n\nconst LEAFLET_CSS = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';\n// SRI for leaflet@1.9.4/dist/leaflet.css (immutable versioned URL) — guards against a CDN compromise.\nconst LEAFLET_CSS_SRI = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';\n\nlet leafletCssRequested = false;\n\nfunction ensureLeafletCss(): void {\n // Module-level guard in addition to the DOM check so two MapWidgets mounting in\n // the same tick can't both append the <link> before either lands in the DOM.\n if (leafletCssRequested || document.getElementById('leaflet-css')) return;\n leafletCssRequested = true;\n const link = document.createElement('link');\n link.id = 'leaflet-css';\n link.rel = 'stylesheet';\n link.href = LEAFLET_CSS;\n link.integrity = LEAFLET_CSS_SRI;\n link.crossOrigin = 'anonymous';\n // Don't leak the current path (workspace/dashboard IDs) to the CDN.\n link.referrerPolicy = 'no-referrer';\n // Leaflet needs its stylesheet for correct tile/marker positioning. If the CDN\n // is unreachable or the SRI check fails the load is otherwise silent, so warn and\n // clear the guard/element to allow a later mount to retry.\n link.onerror = () => {\n console.warn('[bigconsole] Leaflet CSS failed to load; map styling may be broken.');\n leafletCssRequested = false;\n link.remove();\n };\n document.head.appendChild(link);\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst DEFAULT_MARKER_COLOR = 'var(--color-chart-1, #3b82f6)';\n\n/**\n * Marker colours come from widget data and are interpolated into an inline\n * `style` attribute, so restrict them to a safe character set (hex / rgb() /\n * hsl() / css var() / named colours). Anything outside that — or containing\n * style-breaking characters — falls back to the default token colour.\n */\nfunction safeColor(color: unknown): string {\n // Marker data is untrusted: a parser can emit a non-string colour (e.g. a numeric\n // status code) that slips past the lat/lng filter, so guard the type before trim().\n if (typeof color !== 'string') return DEFAULT_MARKER_COLOR;\n const trimmed = color.trim();\n if (trimmed.length > 64) return DEFAULT_MARKER_COLOR;\n if (/[;{}<>\"'\\\\]/.test(trimmed)) return DEFAULT_MARKER_COLOR;\n // Block `url()` so a marker colour can't smuggle an exfiltration beacon\n // (e.g. `url(//evil.com/pixel)`) through the otherwise-permissive char set.\n if (/\\burl\\s*\\(/i.test(trimmed)) return DEFAULT_MARKER_COLOR;\n // Allow `_` (custom-property names) and `/` (modern `rgb(r g b / a)` syntax);\n // the dangerous-char guard above still blocks anything that could break out of\n // the inline style.\n if (!/^[#a-zA-Z0-9(),.%/_\\s-]+$/.test(trimmed)) return DEFAULT_MARKER_COLOR;\n return trimmed;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === 'number' && Number.isFinite(v);\n}\n\n/**\n * Normalize the bound widget data into markers + optional center/zoom. Accepts\n * the documented `{ markers, center, zoom }` shape, or a bare array of markers\n * (parser output), and drops any entry without valid coordinates.\n */\ninterface NormalizedMapData {\n markers: MapMarker[];\n /** Center explicitly supplied by the data payload — wins over markers. */\n center?: { lat: number; lng: number };\n /** Zoom explicitly supplied by the data payload. */\n zoom?: number;\n /** Center from widget config — a static fallback used only when there are no markers. */\n fallbackCenter?: { lat: number; lng: number };\n /** Zoom from widget config — fallback for the no-marker case. */\n fallbackZoom?: number;\n}\n\nfunction asLatLng(v: unknown): { lat: number; lng: number } | undefined {\n const c = v as { lat?: unknown; lng?: unknown } | undefined;\n return c && isFiniteNumber(c.lat) && isFiniteNumber(c.lng) ? { lat: c.lat, lng: c.lng } : undefined;\n}\n\nfunction normalizeMapData(data: MapData | MapMarker[] | undefined, config: Record<string, unknown>): NormalizedMapData {\n // The widget may be handed either the documented { markers, center, zoom } shape\n // or a bare marker array (parser output); narrow once so the rest is type-safe.\n const payload: MapData | undefined = Array.isArray(data) ? undefined : data;\n const rawMarkers: unknown = Array.isArray(data) ? data : data?.markers;\n const markers: MapMarker[] = Array.isArray(rawMarkers)\n ? (rawMarkers as MapMarker[]).filter((m) => m && isFiniteNumber(m.lat) && isFiniteNumber(m.lng))\n : [];\n\n // Data-payload center/zoom are an explicit intent and win over markers. Config\n // center/zoom (e.g. the WidgetRegistry default of {0,0}/2) are only a static\n // fallback for marker-less maps — otherwise that default would pin every marker\n // dataset to the world view instead of framing the markers.\n return {\n markers,\n center: asLatLng(payload?.center),\n zoom: isFiniteNumber(payload?.zoom) ? payload?.zoom : undefined,\n fallbackCenter: asLatLng(config.center),\n fallbackZoom: isFiniteNumber(config.zoom) ? (config.zoom as number) : undefined,\n };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MapWidget: FC<MapWidgetProps> = memo(function MapWidget({ widget, data, onClick, onDrilldown }) {\n const containerRef = useRef<HTMLDivElement>(null);\n const mapRef = useRef<L.Map | null>(null);\n // Signature of the last view we auto-framed to; lets us re-render markers on\n // every data change without snapping the viewport back when the framing\n // inputs (coords/center/zoom) are unchanged — e.g. a refetch of identical data.\n const lastFrameSigRef = useRef<string | null>(null);\n const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading');\n\n const config = useMemo(() => (widget?.config ?? {}) as Record<string, unknown>, [widget?.config]);\n const showMarkers = config.showMarkers !== false;\n const { markers, center, zoom, fallbackCenter, fallbackZoom } = useMemo(\n () => normalizeMapData(data, config),\n [data, config]\n );\n\n // Keep the latest click handler in a ref so marker bindings don't need rebinding.\n // Assign in an effect (not during render) per the React 19 useRef guidance.\n const clickRef = useRef<(marker: MapMarker) => void>(() => undefined);\n useEffect(() => {\n clickRef.current = (marker: MapMarker) => {\n if (onDrilldown) onDrilldown(marker as unknown as Record<string, unknown>);\n else if (onClick) onClick(marker);\n };\n }, [onClick, onDrilldown]);\n\n // Initialise the map once.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return undefined;\n ensureLeafletCss();\n\n let map: L.Map;\n try {\n map = L.map(el, { center: [20, 0], zoom: 2, scrollWheelZoom: true, attributionControl: true });\n const tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© OpenStreetMap contributors',\n maxZoom: 18,\n });\n // Leaflet reports tile failures asynchronously, so addTo() won't throw when\n // tiles are blocked/offline. Surface the error overlay only if a tile errors\n // before any tile has loaded (true offline/blocked), tolerating the odd\n // transient miss once the base map is up.\n let anyTileLoaded = false;\n tiles.on('tileload', () => {\n anyTileLoaded = true;\n // Recover if an early tile miss had shown the offline overlay — a flaky\n // connection that later succeeds shouldn't stay hidden behind it.\n setStatus((s) => (s === 'error' ? 'ready' : s));\n });\n tiles.on('tileerror', () => {\n if (!anyTileLoaded) setStatus('error');\n });\n tiles.addTo(map);\n mapRef.current = map;\n // Fresh map ⇒ force the next marker effect to (re)frame it. Without this the\n // frame-signature ref would survive a remount (e.g. React StrictMode's\n // double-invoke) and the new map would stay stuck at this init view.\n lastFrameSigRef.current = null;\n setStatus('ready');\n } catch {\n setStatus('error');\n return undefined;\n }\n\n // Widgets live in a resizable grid; keep tiles correct as the box changes.\n const ro =\n typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => mapRef.current?.invalidateSize()) : undefined;\n ro?.observe(el);\n const t = window.setTimeout(() => map.invalidateSize(), 80);\n\n return () => {\n window.clearTimeout(t);\n ro?.disconnect();\n map.remove();\n mapRef.current = null;\n };\n }, []);\n\n // Render markers + frame the view whenever the data changes.\n useEffect(() => {\n const map = mapRef.current;\n if (!map) return undefined;\n\n const layer = L.layerGroup();\n if (showMarkers) {\n for (const marker of markers) {\n // Build the pin as a DOM element (not an HTML string) so the marker colour\n // is assigned via the style API and can never be interpreted as markup —\n // defense-in-depth alongside safeColor().\n const pin = document.createElement('div');\n Object.assign(pin.style, {\n width: '14px',\n height: '14px',\n borderRadius: '50% 50% 50% 0',\n transform: 'rotate(-45deg)',\n background: safeColor(marker.color),\n border: '2px solid #fff',\n boxShadow: '0 1px 4px rgba(0,0,0,.4)',\n });\n const icon = L.divIcon({\n className: 'bc-map-marker',\n html: pin,\n iconSize: [14, 14],\n iconAnchor: [7, 14],\n });\n const m = L.marker([marker.lat, marker.lng], { icon }).addTo(layer);\n\n // bindPopup treats a string as HTML; build a text node so marker-supplied\n // text can never be interpreted as markup (defense-in-depth).\n const popupEl = document.createElement('div');\n const title = document.createElement('div');\n title.style.fontWeight = '600';\n title.textContent = marker.popup || marker.name || 'Location';\n popupEl.appendChild(title);\n if (isFiniteNumber(marker.value)) {\n const val = document.createElement('div');\n val.textContent = marker.value.toLocaleString();\n popupEl.appendChild(val);\n }\n m.bindPopup(popupEl);\n m.on('click', () => clickRef.current(marker));\n }\n }\n layer.addTo(map);\n\n // Only (re)frame the view when the framing inputs actually change — otherwise\n // a data-prop reference change (e.g. a refetch returning identical markers)\n // would discard the user's current pan/zoom.\n const frameSig = JSON.stringify({\n m: markers.map((mk) => [mk.lat, mk.lng]),\n c: center ? [center.lat, center.lng] : null,\n z: zoom ?? null,\n fc: fallbackCenter ? [fallbackCenter.lat, fallbackCenter.lng] : null,\n fz: fallbackZoom ?? null,\n });\n if (frameSig !== lastFrameSigRef.current) {\n lastFrameSigRef.current = frameSig;\n // Framing priority: data-payload center → frame the markers → config fallback\n // center (no markers) → config/payload zoom only → world view. Markers win over\n // the config default so a palette-created map frames its data instead of {0,0}.\n if (center) {\n map.setView([center.lat, center.lng], zoom ?? map.getZoom());\n } else if (markers.length === 1) {\n map.setView([markers[0].lat, markers[0].lng], zoom ?? 8);\n } else if (markers.length > 1) {\n const bounds = L.latLngBounds(markers.map((m) => [m.lat, m.lng] as [number, number]));\n map.fitBounds(bounds, { padding: [32, 32], maxZoom: 12 });\n } else if (fallbackCenter) {\n map.setView([fallbackCenter.lat, fallbackCenter.lng], zoom ?? fallbackZoom ?? map.getZoom());\n } else if (isFiniteNumber(zoom ?? fallbackZoom)) {\n map.setZoom((zoom ?? fallbackZoom) as number);\n }\n }\n\n const resize = window.setTimeout(() => map.invalidateSize(), 0);\n\n return () => {\n window.clearTimeout(resize);\n layer.remove();\n };\n }, [markers, center, zoom, fallbackCenter, fallbackZoom, showMarkers]);\n\n return (\n <div className=\"relative h-full w-full overflow-hidden rounded-lg bg-bg-muted\">\n <div ref={containerRef} className=\"h-full w-full\" />\n\n {status !== 'error' && markers.length === 0 && (\n <div className=\"pointer-events-none absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted/70 text-sm text-text-secondary\">\n <div className=\"text-center\">\n <svg\n className=\"mx-auto mb-2 h-12 w-12 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=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"\n />\n </svg>\n <p>No map data</p>\n </div>\n </div>\n )}\n\n {status === 'error' && (\n <div className=\"absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted px-4 text-center text-sm text-text-secondary\">\n The map needs an internet connection and couldn’t load.\n </div>\n )}\n </div>\n );\n});\n\nexport default MapWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"MapWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/map-widget/MapWidget.tsx"],"sourcesContent":["/**\n * MapWidget Component\n *\n * Interactive geographic map (Leaflet + OpenStreetMap tiles) with data-bound\n * markers. Leaflet's stylesheet is loaded from a CDN at runtime (with SRI) because\n * the build-time MFE library bundle does not ship a component's CSS to the host\n * shell — consistent with the map being an inherently online feature (tiles).\n */\n\nimport { type FC, memo, useEffect, useMemo, useRef, useState } from 'react';\nimport L from 'leaflet';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MapWidgetProps {\n widget: Widget;\n data?: MapData;\n onClick?: (marker: MapMarker) => void;\n /** Drilldown handler - receives selected marker data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MapMarker {\n name: string;\n lat: number;\n lng: number;\n value?: number;\n popup?: string;\n color?: string;\n}\n\n/** Minimal GeoJSON FeatureCollection shape (we only read geometry + properties). */\nexport interface GeoJsonFeatureCollection {\n type: 'FeatureCollection';\n features: Array<{\n type: 'Feature';\n id?: string | number;\n properties?: Record<string, unknown> | null;\n geometry: unknown;\n }>;\n}\n\n/** A region value used to shade a choropleth (joined to a GeoJSON feature). */\nexport interface MapRegionValue {\n /** Join key — matched against the feature's id field. */\n id: string;\n /** Numeric value driving the fill intensity. */\n value: number;\n /** Optional display name for the popup. */\n name?: string;\n}\n\nexport interface MapData {\n center?: { lat: number; lng: number };\n zoom?: number;\n markers?: MapMarker[];\n /** GeoJSON regions for choropleth mode. */\n geoJson?: GeoJsonFeatureCollection;\n /** Per-region values joined to GeoJSON features for choropleth shading. */\n regions?: MapRegionValue[];\n}\n\n// ============================================================================\n// Leaflet CSS (runtime CDN injection)\n// ============================================================================\n\nconst LEAFLET_CSS = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';\n// SRI for leaflet@1.9.4/dist/leaflet.css (immutable versioned URL) — guards against a CDN compromise.\nconst LEAFLET_CSS_SRI = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';\n\nlet leafletCssRequested = false;\n\nfunction ensureLeafletCss(): void {\n // Module-level guard in addition to the DOM check so two MapWidgets mounting in\n // the same tick can't both append the <link> before either lands in the DOM.\n if (leafletCssRequested || document.getElementById('leaflet-css')) return;\n leafletCssRequested = true;\n const link = document.createElement('link');\n link.id = 'leaflet-css';\n link.rel = 'stylesheet';\n link.href = LEAFLET_CSS;\n link.integrity = LEAFLET_CSS_SRI;\n link.crossOrigin = 'anonymous';\n // Don't leak the current path (workspace/dashboard IDs) to the CDN.\n link.referrerPolicy = 'no-referrer';\n // Leaflet needs its stylesheet for correct tile/marker positioning. If the CDN\n // is unreachable or the SRI check fails the load is otherwise silent, so warn and\n // clear the guard/element to allow a later mount to retry.\n link.onerror = () => {\n console.warn('[bigconsole] Leaflet CSS failed to load; map styling may be broken.');\n leafletCssRequested = false;\n link.remove();\n };\n document.head.appendChild(link);\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst DEFAULT_MARKER_COLOR = 'var(--color-chart-1, #3b82f6)';\n\n/**\n * Marker colours come from widget data and are interpolated into an inline\n * `style` attribute, so restrict them to a safe character set (hex / rgb() /\n * hsl() / css var() / named colours). Anything outside that — or containing\n * style-breaking characters — falls back to the default token colour.\n */\nfunction safeColor(color: unknown): string {\n // Marker data is untrusted: a parser can emit a non-string colour (e.g. a numeric\n // status code) that slips past the lat/lng filter, so guard the type before trim().\n if (typeof color !== 'string') return DEFAULT_MARKER_COLOR;\n const trimmed = color.trim();\n if (trimmed.length > 64) return DEFAULT_MARKER_COLOR;\n if (/[;{}<>\"'\\\\]/.test(trimmed)) return DEFAULT_MARKER_COLOR;\n // Block `url()` so a marker colour can't smuggle an exfiltration beacon\n // (e.g. `url(//evil.com/pixel)`) through the otherwise-permissive char set.\n if (/\\burl\\s*\\(/i.test(trimmed)) return DEFAULT_MARKER_COLOR;\n // Allow `_` (custom-property names) and `/` (modern `rgb(r g b / a)` syntax);\n // the dangerous-char guard above still blocks anything that could break out of\n // the inline style.\n if (!/^[#a-zA-Z0-9(),.%/_\\s-]+$/.test(trimmed)) return DEFAULT_MARKER_COLOR;\n return trimmed;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === 'number' && Number.isFinite(v);\n}\n\n/**\n * Normalize the bound widget data into markers + optional center/zoom. Accepts\n * the documented `{ markers, center, zoom }` shape, or a bare array of markers\n * (parser output), and drops any entry without valid coordinates.\n */\ninterface NormalizedMapData {\n markers: MapMarker[];\n /** Center explicitly supplied by the data payload — wins over markers. */\n center?: { lat: number; lng: number };\n /** Zoom explicitly supplied by the data payload. */\n zoom?: number;\n /** Center from widget config — a static fallback used only when there are no markers. */\n fallbackCenter?: { lat: number; lng: number };\n /** Zoom from widget config — fallback for the no-marker case. */\n fallbackZoom?: number;\n /** GeoJSON for choropleth mode. */\n geoJson?: GeoJsonFeatureCollection;\n /** region id → { value, name } map for choropleth shading. */\n regionValues: Map<string, { value: number; name?: string }>;\n}\n\nfunction asLatLng(v: unknown): { lat: number; lng: number } | undefined {\n const c = v as { lat?: unknown; lng?: unknown } | undefined;\n return c && isFiniteNumber(c.lat) && isFiniteNumber(c.lng) ? { lat: c.lat, lng: c.lng } : undefined;\n}\n\nfunction normalizeMapData(data: MapData | MapMarker[] | undefined, config: Record<string, unknown>): NormalizedMapData {\n // The widget may be handed either the documented { markers, center, zoom } shape\n // or a bare marker array (parser output); narrow once so the rest is type-safe.\n const payload: MapData | undefined = Array.isArray(data) ? undefined : data;\n const rawMarkers: unknown = Array.isArray(data) ? data : data?.markers;\n const markers: MapMarker[] = Array.isArray(rawMarkers)\n ? (rawMarkers as MapMarker[]).filter((m) => m && isFiniteNumber(m.lat) && isFiniteNumber(m.lng))\n : [];\n\n // Choropleth inputs: GeoJSON (from data payload or config) + a region→value join.\n const geoJson = (payload?.geoJson ?? (config.geoJson as GeoJsonFeatureCollection | undefined)) || undefined;\n const regionIdField = (config.regionIdField as string) || 'id';\n const regionValues = new Map<string, { value: number; name?: string }>();\n const rawRegions: unknown = payload?.regions ?? config.regions;\n if (Array.isArray(rawRegions)) {\n for (const r of rawRegions as Array<Record<string, unknown>>) {\n if (!r) continue;\n const id = r[regionIdField] ?? r.id ?? r.region ?? r.code;\n const value = r.value ?? r.count ?? r.total;\n if (id != null && isFiniteNumber(value)) {\n regionValues.set(String(id), { value, name: typeof r.name === 'string' ? r.name : undefined });\n }\n }\n }\n\n // Data-payload center/zoom are an explicit intent and win over markers. Config\n // center/zoom (e.g. the WidgetRegistry default of {0,0}/2) are only a static\n // fallback for marker-less maps — otherwise that default would pin every marker\n // dataset to the world view instead of framing the markers.\n return {\n markers,\n center: asLatLng(payload?.center),\n zoom: isFiniteNumber(payload?.zoom) ? payload?.zoom : undefined,\n fallbackCenter: asLatLng(config.center),\n fallbackZoom: isFiniteNumber(config.zoom) ? (config.zoom as number) : undefined,\n geoJson: geoJson && Array.isArray(geoJson.features) ? geoJson : undefined,\n regionValues,\n };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MapWidget: FC<MapWidgetProps> = memo(function MapWidget({ widget, data, onClick, onDrilldown }) {\n const containerRef = useRef<HTMLDivElement>(null);\n const mapRef = useRef<L.Map | null>(null);\n // Signature of the last view we auto-framed to; lets us re-render markers on\n // every data change without snapping the viewport back when the framing\n // inputs (coords/center/zoom) are unchanged — e.g. a refetch of identical data.\n const lastFrameSigRef = useRef<string | null>(null);\n const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading');\n\n const config = useMemo(() => (widget?.config ?? {}) as Record<string, unknown>, [widget?.config]);\n const showMarkers = config.showMarkers !== false;\n const regionIdField = (config.regionIdField as string) || 'id';\n const { markers, center, zoom, fallbackCenter, fallbackZoom, geoJson, regionValues } = useMemo(\n () => normalizeMapData(data, config),\n [data, config]\n );\n const hasChoropleth = Boolean(geoJson && regionValues.size > 0);\n\n // Keep the latest click handler in a ref so marker bindings don't need rebinding.\n // Assign in an effect (not during render) per the React 19 useRef guidance.\n const clickRef = useRef<(marker: MapMarker) => void>(() => undefined);\n useEffect(() => {\n clickRef.current = (marker: MapMarker) => {\n if (onDrilldown) onDrilldown(marker as unknown as Record<string, unknown>);\n else if (onClick) onClick(marker);\n };\n }, [onClick, onDrilldown]);\n\n // Initialise the map once.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return undefined;\n ensureLeafletCss();\n\n let map: L.Map;\n try {\n map = L.map(el, { center: [20, 0], zoom: 2, scrollWheelZoom: true, attributionControl: true });\n const tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© OpenStreetMap contributors',\n maxZoom: 18,\n });\n // Leaflet reports tile failures asynchronously, so addTo() won't throw when\n // tiles are blocked/offline. Surface the error overlay only if a tile errors\n // before any tile has loaded (true offline/blocked), tolerating the odd\n // transient miss once the base map is up.\n let anyTileLoaded = false;\n tiles.on('tileload', () => {\n anyTileLoaded = true;\n // Recover if an early tile miss had shown the offline overlay — a flaky\n // connection that later succeeds shouldn't stay hidden behind it.\n setStatus((s) => (s === 'error' ? 'ready' : s));\n });\n tiles.on('tileerror', () => {\n if (!anyTileLoaded) setStatus('error');\n });\n tiles.addTo(map);\n mapRef.current = map;\n // Fresh map ⇒ force the next marker effect to (re)frame it. Without this the\n // frame-signature ref would survive a remount (e.g. React StrictMode's\n // double-invoke) and the new map would stay stuck at this init view.\n lastFrameSigRef.current = null;\n setStatus('ready');\n } catch {\n setStatus('error');\n return undefined;\n }\n\n // Widgets live in a resizable grid; keep tiles correct as the box changes.\n const ro =\n typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => mapRef.current?.invalidateSize()) : undefined;\n ro?.observe(el);\n const t = window.setTimeout(() => map.invalidateSize(), 80);\n\n return () => {\n window.clearTimeout(t);\n ro?.disconnect();\n map.remove();\n mapRef.current = null;\n };\n }, []);\n\n // Render markers + frame the view whenever the data changes.\n useEffect(() => {\n const map = mapRef.current;\n if (!map) return undefined;\n\n const layer = L.layerGroup();\n\n // Choropleth: shade GeoJSON regions by a single-hue intensity ramp (the\n // semantic `info` token at varying opacity) joined on the region id field.\n let geoLayer: L.GeoJSON | null = null;\n if (geoJson && regionValues.size > 0) {\n const vals = Array.from(regionValues.values()).map((v) => v.value);\n const min = Math.min(...vals);\n const max = Math.max(...vals);\n const span = max - min || 1;\n const FILL = 'var(--color-status-info, #3b82f6)';\n\n const lookup = (feature: { id?: string | number; properties?: Record<string, unknown> | null }) => {\n const props = feature.properties ?? {};\n const joinId = feature.id ?? props[regionIdField] ?? props.id ?? props.code;\n return joinId != null ? regionValues.get(String(joinId)) : undefined;\n };\n\n geoLayer = L.geoJSON(geoJson as unknown as Parameters<typeof L.geoJSON>[0], {\n style: (feature) => {\n const hit = feature ? lookup(feature) : undefined;\n const norm = hit ? (hit.value - min) / span : 0;\n return {\n fillColor: FILL,\n fillOpacity: hit ? 0.2 + norm * 0.65 : 0.05,\n color: 'var(--color-border-default, #cbd5e1)',\n weight: 1,\n };\n },\n onEachFeature: (feature, lyr) => {\n const hit = lookup(feature);\n const name =\n hit?.name ??\n (typeof feature.properties?.name === 'string' ? (feature.properties.name as string) : undefined) ??\n String(feature.id ?? '');\n const popupEl = document.createElement('div');\n const title = document.createElement('div');\n title.style.fontWeight = '600';\n title.textContent = name || 'Region';\n popupEl.appendChild(title);\n if (hit) {\n const val = document.createElement('div');\n val.textContent = hit.value.toLocaleString();\n popupEl.appendChild(val);\n }\n lyr.bindPopup(popupEl);\n lyr.on('click', () => clickRef.current({ name, lat: NaN, lng: NaN, value: hit?.value } as MapMarker));\n },\n });\n geoLayer.addTo(layer);\n }\n\n if (showMarkers) {\n for (const marker of markers) {\n // Build the pin as a DOM element (not an HTML string) so the marker colour\n // is assigned via the style API and can never be interpreted as markup —\n // defense-in-depth alongside safeColor().\n const pin = document.createElement('div');\n Object.assign(pin.style, {\n width: '14px',\n height: '14px',\n borderRadius: '50% 50% 50% 0',\n transform: 'rotate(-45deg)',\n background: safeColor(marker.color),\n border: '2px solid #fff',\n boxShadow: '0 1px 4px rgba(0,0,0,.4)',\n });\n const icon = L.divIcon({\n className: 'bc-map-marker',\n html: pin,\n iconSize: [14, 14],\n iconAnchor: [7, 14],\n });\n const m = L.marker([marker.lat, marker.lng], { icon }).addTo(layer);\n\n // bindPopup treats a string as HTML; build a text node so marker-supplied\n // text can never be interpreted as markup (defense-in-depth).\n const popupEl = document.createElement('div');\n const title = document.createElement('div');\n title.style.fontWeight = '600';\n title.textContent = marker.popup || marker.name || 'Location';\n popupEl.appendChild(title);\n if (isFiniteNumber(marker.value)) {\n const val = document.createElement('div');\n val.textContent = marker.value.toLocaleString();\n popupEl.appendChild(val);\n }\n m.bindPopup(popupEl);\n m.on('click', () => clickRef.current(marker));\n }\n }\n layer.addTo(map);\n\n // Only (re)frame the view when the framing inputs actually change — otherwise\n // a data-prop reference change (e.g. a refetch returning identical markers)\n // would discard the user's current pan/zoom.\n const frameSig = JSON.stringify({\n m: markers.map((mk) => [mk.lat, mk.lng]),\n c: center ? [center.lat, center.lng] : null,\n z: zoom ?? null,\n fc: fallbackCenter ? [fallbackCenter.lat, fallbackCenter.lng] : null,\n fz: fallbackZoom ?? null,\n g: geoJson ? regionValues.size : 0,\n });\n if (frameSig !== lastFrameSigRef.current) {\n lastFrameSigRef.current = frameSig;\n // Framing priority: data-payload center → frame the markers → frame the\n // choropleth regions → config fallback center → zoom only → world view.\n // Markers/regions win over the config default so a palette-created map frames\n // its data instead of {0,0}.\n if (center) {\n map.setView([center.lat, center.lng], zoom ?? map.getZoom());\n } else if (!markers.length && geoLayer) {\n const b = geoLayer.getBounds();\n if (b.isValid()) map.fitBounds(b, { padding: [24, 24], maxZoom: 12 });\n } else if (markers.length === 1) {\n map.setView([markers[0].lat, markers[0].lng], zoom ?? 8);\n } else if (markers.length > 1) {\n const bounds = L.latLngBounds(markers.map((m) => [m.lat, m.lng] as [number, number]));\n map.fitBounds(bounds, { padding: [32, 32], maxZoom: 12 });\n } else if (fallbackCenter) {\n map.setView([fallbackCenter.lat, fallbackCenter.lng], zoom ?? fallbackZoom ?? map.getZoom());\n } else if (isFiniteNumber(zoom ?? fallbackZoom)) {\n map.setZoom((zoom ?? fallbackZoom) as number);\n }\n }\n\n const resize = window.setTimeout(() => map.invalidateSize(), 0);\n\n return () => {\n window.clearTimeout(resize);\n layer.remove();\n };\n }, [markers, center, zoom, fallbackCenter, fallbackZoom, showMarkers, geoJson, regionValues, regionIdField]);\n\n return (\n <div className=\"relative h-full w-full overflow-hidden rounded-lg bg-bg-muted\">\n <div ref={containerRef} className=\"h-full w-full\" />\n\n {status !== 'error' && markers.length === 0 && !hasChoropleth && (\n <div className=\"pointer-events-none absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted/70 text-sm text-text-secondary\">\n <div className=\"text-center\">\n <svg\n className=\"mx-auto mb-2 h-12 w-12 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=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"\n />\n </svg>\n <p>No map data</p>\n </div>\n </div>\n )}\n\n {status === 'error' && (\n <div className=\"absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted px-4 text-center text-sm text-text-secondary\">\n The map needs an internet connection and couldn’t load.\n </div>\n )}\n </div>\n );\n});\n\nexport default MapWidget;\n"],"mappings":";;;;AAqEA,IAAM,IAAc,oDAEd,IAAkB,uDAEpB,IAAsB;AAE1B,SAAS,IAAyB;AAGhC,KAAI,KAAuB,SAAS,eAAe,cAAc,CAAE;AACnE,KAAsB;CACtB,IAAM,IAAO,SAAS,cAAc,OAAO;AAgB3C,CAfA,EAAK,KAAK,eACV,EAAK,MAAM,cACX,EAAK,OAAO,GACZ,EAAK,YAAY,GACjB,EAAK,cAAc,aAEnB,EAAK,iBAAiB,eAItB,EAAK,gBAAgB;AAGnB,EAFA,QAAQ,KAAK,sEAAsE,EACnF,IAAsB,IACtB,EAAK,QAAQ;IAEf,SAAS,KAAK,YAAY,EAAK;;AAOjC,IAAM,IAAuB;AAQ7B,SAAS,EAAU,GAAwB;AAGzC,KAAI,OAAO,KAAU,SAAU,QAAO;CACtC,IAAM,IAAU,EAAM,MAAM;AAU5B,QATI,EAAQ,SAAS,MACjB,cAAc,KAAK,EAAQ,IAG3B,cAAc,KAAK,EAAQ,IAI3B,CAAC,4BAA4B,KAAK,EAAQ,GAAS,IAChD;;AAGT,SAAS,EAAe,GAAyB;AAC/C,QAAO,OAAO,KAAM,YAAY,OAAO,SAAS,EAAE;;AAwBpD,SAAS,EAAS,GAAsD;CACtE,IAAM,IAAI;AACV,QAAO,KAAK,EAAe,EAAE,IAAI,IAAI,EAAe,EAAE,IAAI,GAAG;EAAE,KAAK,EAAE;EAAK,KAAK,EAAE;EAAK,GAAG,KAAA;;AAG5F,SAAS,EAAiB,GAAyC,GAAoD;CAGrH,IAAM,IAA+B,MAAM,QAAQ,EAAK,GAAG,KAAA,IAAY,GACjE,IAAsB,MAAM,QAAQ,EAAK,GAAG,IAAO,GAAM,SACzD,IAAuB,MAAM,QAAQ,EAAW,GACjD,EAA2B,QAAQ,MAAM,KAAK,EAAe,EAAE,IAAI,IAAI,EAAe,EAAE,IAAI,CAAC,GAC9F,EAAE,EAGA,KAAW,GAAS,WAAY,EAAO,YAAqD,KAAA,GAC5F,IAAiB,EAAO,iBAA4B,MACpD,oBAAe,IAAI,KAA+C,EAClE,IAAsB,GAAS,WAAW,EAAO;AACvD,KAAI,MAAM,QAAQ,EAAW,CAC3B,MAAK,IAAM,KAAK,GAA8C;AAC5D,MAAI,CAAC,EAAG;EACR,IAAM,IAAK,EAAE,MAAkB,EAAE,MAAM,EAAE,UAAU,EAAE,MAC/C,IAAQ,EAAE,SAAS,EAAE,SAAS,EAAE;AACtC,EAAI,KAAM,QAAQ,EAAe,EAAM,IACrC,EAAa,IAAI,OAAO,EAAG,EAAE;GAAE;GAAO,MAAM,OAAO,EAAE,QAAS,WAAW,EAAE,OAAO,KAAA;GAAW,CAAC;;AASpG,QAAO;EACL;EACA,QAAQ,EAAS,GAAS,OAAO;EACjC,MAAM,EAAe,GAAS,KAAK,GAAG,GAAS,OAAO,KAAA;EACtD,gBAAgB,EAAS,EAAO,OAAO;EACvC,cAAc,EAAe,EAAO,KAAK,GAAI,EAAO,OAAkB,KAAA;EACtE,SAAS,KAAW,MAAM,QAAQ,EAAQ,SAAS,GAAG,IAAU,KAAA;EAChE;EACD;;AAOH,IAAa,IAAgC,EAAK,SAAmB,EAAE,WAAQ,SAAM,YAAS,kBAAe;CAC3G,IAAM,IAAe,EAAuB,KAAK,EAC3C,IAAS,EAAqB,KAAK,EAInC,IAAkB,EAAsB,KAAK,EAC7C,CAAC,GAAQ,KAAa,EAAwC,UAAU,EAExE,IAAS,QAAe,GAAQ,UAAU,EAAE,EAA8B,CAAC,GAAQ,OAAO,CAAC,EAC3F,IAAc,EAAO,gBAAgB,IACrC,IAAiB,EAAO,iBAA4B,MACpD,EAAE,YAAS,WAAQ,SAAM,mBAAgB,iBAAc,YAAS,oBAAiB,QAC/E,EAAiB,GAAM,EAAO,EACpC,CAAC,GAAM,EAAO,CACf,EACK,IAAgB,GAAQ,KAAW,EAAa,OAAO,IAIvD,IAAW,QAA0C,KAAA,EAAU;AAyMrE,QAxMA,QAAgB;AACd,IAAS,WAAW,MAAsB;AACxC,GAAI,IAAa,EAAY,EAA6C,GACjE,KAAS,EAAQ,EAAO;;IAElC,CAAC,GAAS,EAAY,CAAC,EAG1B,QAAgB;EACd,IAAM,IAAK,EAAa;AACxB,MAAI,CAAC,EAAI;AACT,KAAkB;EAElB,IAAI;AACJ,MAAI;AACF,OAAM,EAAE,IAAI,GAAI;IAAE,QAAQ,CAAC,IAAI,EAAE;IAAE,MAAM;IAAG,iBAAiB;IAAM,oBAAoB;IAAM,CAAC;GAC9F,IAAM,IAAQ,EAAE,UAAU,sDAAsD;IAC9E,aAAa;IACb,SAAS;IACV,CAAC,EAKE,IAAgB;AAgBpB,GAfA,EAAM,GAAG,kBAAkB;AAIzB,IAHA,IAAgB,IAGhB,GAAW,MAAO,MAAM,UAAU,UAAU,EAAG;KAC/C,EACF,EAAM,GAAG,mBAAmB;AAC1B,IAAK,KAAe,EAAU,QAAQ;KACtC,EACF,EAAM,MAAM,EAAI,EAChB,EAAO,UAAU,GAIjB,EAAgB,UAAU,MAC1B,EAAU,QAAQ;UACZ;AACN,KAAU,QAAQ;AAClB;;EAIF,IAAM,IACJ,OAAO,iBAAmB,MAAc,IAAI,qBAAqB,EAAO,SAAS,gBAAgB,CAAC,GAAG,KAAA;AACvG,KAAI,QAAQ,EAAG;EACf,IAAM,IAAI,OAAO,iBAAiB,EAAI,gBAAgB,EAAE,GAAG;AAE3D,eAAa;AAIX,GAHA,OAAO,aAAa,EAAE,EACtB,GAAI,YAAY,EAChB,EAAI,QAAQ,EACZ,EAAO,UAAU;;IAElB,EAAE,CAAC,EAGN,QAAgB;EACd,IAAM,IAAM,EAAO;AACnB,MAAI,CAAC,EAAK;EAEV,IAAM,IAAQ,EAAE,YAAY,EAIxB,IAA6B;AACjC,MAAI,KAAW,EAAa,OAAO,GAAG;GACpC,IAAM,IAAO,MAAM,KAAK,EAAa,QAAQ,CAAC,CAAC,KAAK,MAAM,EAAE,MAAM,EAC5D,IAAM,KAAK,IAAI,GAAG,EAAK,EAEvB,IADM,KAAK,IAAI,GAAG,EAAK,GACV,KAAO,GAGpB,KAAU,MAAmF;IACjG,IAAM,IAAQ,EAAQ,cAAc,EAAE,EAChC,IAAS,EAAQ,MAAM,EAAM,MAAkB,EAAM,MAAM,EAAM;AACvE,WAAO,KAAU,OAA0C,KAAA,IAAnC,EAAa,IAAI,OAAO,EAAO,CAAC;;AAkC1D,GA/BA,IAAW,EAAE,QAAQ,GAAuD;IAC1E,QAAQ,MAAY;KAClB,IAAM,IAAM,IAAU,EAAO,EAAQ,GAAG,KAAA,GAClC,IAAO,KAAO,EAAI,QAAQ,KAAO,IAAO;AAC9C,YAAO;MACL,WAAW;MACX,aAAa,IAAM,KAAM,IAAO,MAAO;MACvC,OAAO;MACP,QAAQ;MACT;;IAEH,gBAAgB,GAAS,MAAQ;KAC/B,IAAM,IAAM,EAAO,EAAQ,EACrB,IACJ,GAAK,SACJ,OAAO,EAAQ,YAAY,QAAS,WAAY,EAAQ,WAAW,OAAkB,KAAA,MACtF,OAAO,EAAQ,MAAM,GAAG,EACpB,IAAU,SAAS,cAAc,MAAM,EACvC,IAAQ,SAAS,cAAc,MAAM;AAI3C,SAHA,EAAM,MAAM,aAAa,OACzB,EAAM,cAAc,KAAQ,UAC5B,EAAQ,YAAY,EAAM,EACtB,GAAK;MACP,IAAM,IAAM,SAAS,cAAc,MAAM;AAEzC,MADA,EAAI,cAAc,EAAI,MAAM,gBAAgB,EAC5C,EAAQ,YAAY,EAAI;;AAG1B,KADA,EAAI,UAAU,EAAQ,EACtB,EAAI,GAAG,eAAe,EAAS,QAAQ;MAAE;MAAM,KAAK;MAAK,KAAK;MAAK,OAAO,GAAK;MAAO,CAAc,CAAC;;IAExG,CAAC,EACF,EAAS,MAAM,EAAM;;AAGvB,MAAI,EACF,MAAK,IAAM,KAAU,GAAS;GAI5B,IAAM,IAAM,SAAS,cAAc,MAAM;AACzC,UAAO,OAAO,EAAI,OAAO;IACvB,OAAO;IACP,QAAQ;IACR,cAAc;IACd,WAAW;IACX,YAAY,EAAU,EAAO,MAAM;IACnC,QAAQ;IACR,WAAW;IACZ,CAAC;GACF,IAAM,IAAO,EAAE,QAAQ;IACrB,WAAW;IACX,MAAM;IACN,UAAU,CAAC,IAAI,GAAG;IAClB,YAAY,CAAC,GAAG,GAAG;IACpB,CAAC,EACI,IAAI,EAAE,OAAO,CAAC,EAAO,KAAK,EAAO,IAAI,EAAE,EAAE,SAAM,CAAC,CAAC,MAAM,EAAM,EAI7D,IAAU,SAAS,cAAc,MAAM,EACvC,IAAQ,SAAS,cAAc,MAAM;AAI3C,OAHA,EAAM,MAAM,aAAa,OACzB,EAAM,cAAc,EAAO,SAAS,EAAO,QAAQ,YACnD,EAAQ,YAAY,EAAM,EACtB,EAAe,EAAO,MAAM,EAAE;IAChC,IAAM,IAAM,SAAS,cAAc,MAAM;AAEzC,IADA,EAAI,cAAc,EAAO,MAAM,gBAAgB,EAC/C,EAAQ,YAAY,EAAI;;AAG1B,GADA,EAAE,UAAU,EAAQ,EACpB,EAAE,GAAG,eAAe,EAAS,QAAQ,EAAO,CAAC;;AAGjD,IAAM,MAAM,EAAI;EAKhB,IAAM,IAAW,KAAK,UAAU;GAC9B,GAAG,EAAQ,KAAK,MAAO,CAAC,EAAG,KAAK,EAAG,IAAI,CAAC;GACxC,GAAG,IAAS,CAAC,EAAO,KAAK,EAAO,IAAI,GAAG;GACvC,GAAG,KAAQ;GACX,IAAI,IAAiB,CAAC,EAAe,KAAK,EAAe,IAAI,GAAG;GAChE,IAAI,KAAgB;GACpB,GAAG,IAAU,EAAa,OAAO;GAClC,CAAC;AACF,MAAI,MAAa,EAAgB,QAM/B,KALA,EAAgB,UAAU,GAKtB,EACF,GAAI,QAAQ,CAAC,EAAO,KAAK,EAAO,IAAI,EAAE,KAAQ,EAAI,SAAS,CAAC;WACnD,CAAC,EAAQ,UAAU,GAAU;GACtC,IAAM,IAAI,EAAS,WAAW;AAC9B,GAAI,EAAE,SAAS,IAAE,EAAI,UAAU,GAAG;IAAE,SAAS,CAAC,IAAI,GAAG;IAAE,SAAS;IAAI,CAAC;aAC5D,EAAQ,WAAW,EAC5B,GAAI,QAAQ,CAAC,EAAQ,GAAG,KAAK,EAAQ,GAAG,IAAI,EAAE,KAAQ,EAAE;WAC/C,EAAQ,SAAS,GAAG;GAC7B,IAAM,IAAS,EAAE,aAAa,EAAQ,KAAK,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,CAAqB,CAAC;AACrF,KAAI,UAAU,GAAQ;IAAE,SAAS,CAAC,IAAI,GAAG;IAAE,SAAS;IAAI,CAAC;SAChD,IACT,EAAI,QAAQ,CAAC,EAAe,KAAK,EAAe,IAAI,EAAE,KAAQ,KAAgB,EAAI,SAAS,CAAC,GACnF,EAAe,KAAQ,EAAa,IAC7C,EAAI,QAAS,KAAQ,EAAwB;EAIjD,IAAM,IAAS,OAAO,iBAAiB,EAAI,gBAAgB,EAAE,EAAE;AAE/D,eAAa;AAEX,GADA,OAAO,aAAa,EAAO,EAC3B,EAAM,QAAQ;;IAEf;EAAC;EAAS;EAAQ;EAAM;EAAgB;EAAc;EAAa;EAAS;EAAc;EAAc,CAAC,EAG1G,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,KAAK;IAAc,WAAU;IAAkB,CAAA;GAEnD,MAAW,WAAW,EAAQ,WAAW,KAAK,CAAC,KAC9C,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MACE,WAAU;MACV,MAAK;MACL,QAAO;MACP,SAAQ;gBAJV,CAME,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA,EACF,kBAAC,QAAD;OACE,eAAc;OACd,gBAAe;OACf,aAAa;OACb,GAAE;OACF,CAAA,CACE;SACN,kBAAC,KAAD,EAAA,UAAG,eAAe,CAAA,CACd;;IACF,CAAA;GAGP,MAAW,WACV,kBAAC,OAAD;IAAK,WAAU;cAAqH;IAE9H,CAAA;GAEJ;;EAER"}