@burdenoff/microfe-bigconsole 2026.613.4 → 2026.613.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,13 +21,24 @@ function m(e) {
21
21
  return typeof e == "number" && Number.isFinite(e);
22
22
  }
23
23
  function h(e) {
24
+ return e >= .66 ? "var(--color-status-error)" : e >= .33 ? "var(--color-status-warning)" : "var(--color-status-info)";
25
+ }
26
+ function g(e) {
27
+ let t = document.createElement("div"), n = document.createElement("div");
28
+ if (n.style.fontWeight = "600", n.textContent = e.popup || e.name || "Location", t.appendChild(n), m(e.value)) {
29
+ let n = document.createElement("div");
30
+ n.textContent = e.value.toLocaleString(), t.appendChild(n);
31
+ }
32
+ return t;
33
+ }
34
+ function _(e) {
24
35
  let t = e;
25
36
  return t && m(t.lat) && m(t.lng) ? {
26
37
  lat: t.lat,
27
38
  lng: t.lng
28
39
  } : void 0;
29
40
  }
30
- function g(e, t) {
41
+ function v(e, t) {
31
42
  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
43
  if (Array.isArray(c)) for (let e of c) {
33
44
  if (!e) continue;
@@ -39,18 +50,18 @@ function g(e, t) {
39
50
  }
40
51
  return {
41
52
  markers: i,
42
- center: h(n?.center),
53
+ center: _(n?.center),
43
54
  zoom: m(n?.zoom) ? n?.zoom : void 0,
44
- fallbackCenter: h(t.center),
55
+ fallbackCenter: _(t.center),
45
56
  fallbackZoom: m(t.zoom) ? t.zoom : void 0,
46
57
  geoJson: a && Array.isArray(a.features) ? a : void 0,
47
58
  regionValues: s
48
59
  };
49
60
  }
50
- var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
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);
61
+ var y = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
62
+ let f = r(null), _ = r(null), y = r(null), [b, x] = i("loading"), S = n(() => e?.config ?? {}, [e?.config]), C = S.showMarkers !== !1, w = S.mapMode === "heat", T = S.regionIdField || "id", { markers: E, center: D, zoom: O, fallbackCenter: k, fallbackZoom: A, geoJson: j, regionValues: M } = n(() => v(c, S), [c, S]), N = !!(j && M.size > 0), P = r(() => void 0);
52
63
  return t(() => {
53
- j.current = (e) => {
64
+ P.current = (e) => {
54
65
  u ? u(e) : l && l(e);
55
66
  };
56
67
  }, [l, u]), t(() => {
@@ -70,30 +81,30 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
70
81
  maxZoom: 18
71
82
  }), r = !1;
72
83
  n.on("tileload", () => {
73
- r = !0, y((e) => e === "error" ? "ready" : e);
84
+ r = !0, x((e) => e === "error" ? "ready" : e);
74
85
  }), n.on("tileerror", () => {
75
- r || y("error");
76
- }), n.addTo(t), h.current = t, _.current = null, y("ready");
86
+ r || x("error");
87
+ }), n.addTo(t), _.current = t, y.current = null, x("ready");
77
88
  } catch {
78
- y("error");
89
+ x("error");
79
90
  return;
80
91
  }
81
- let n = typeof ResizeObserver < "u" ? new ResizeObserver(() => h.current?.invalidateSize()) : void 0;
92
+ let n = typeof ResizeObserver < "u" ? new ResizeObserver(() => _.current?.invalidateSize()) : void 0;
82
93
  n?.observe(e);
83
94
  let r = window.setTimeout(() => t.invalidateSize(), 80);
84
95
  return () => {
85
- window.clearTimeout(r), n?.disconnect(), t.remove(), h.current = null;
96
+ window.clearTimeout(r), n?.disconnect(), t.remove(), _.current = null;
86
97
  };
87
98
  }, []), t(() => {
88
- let e = h.current;
99
+ let e = _.current;
89
100
  if (!e) return;
90
101
  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));
102
+ if (j && M.size > 0) {
103
+ let e = Array.from(M.values()).map((e) => e.value), r = Math.min(...e), i = Math.max(...e) - r || 1, a = (e) => {
104
+ let t = e.properties ?? {}, n = e.id ?? t[T] ?? t.id ?? t.code;
105
+ return n == null ? void 0 : M.get(String(n));
95
106
  };
96
- n = s.geoJSON(O, {
107
+ n = s.geoJSON(j, {
97
108
  style: (e) => {
98
109
  let t = e ? a(e) : void 0, n = t ? (t.value - r) / i : 0;
99
110
  return {
@@ -109,7 +120,7 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
109
120
  let e = document.createElement("div");
110
121
  e.textContent = n.value.toLocaleString(), i.appendChild(e);
111
122
  }
112
- t.bindPopup(i), t.on("click", () => j.current({
123
+ t.bindPopup(i), t.on("click", () => P.current({
113
124
  name: r,
114
125
  lat: NaN,
115
126
  lng: NaN,
@@ -118,7 +129,18 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
118
129
  }
119
130
  }), n.addTo(t);
120
131
  }
121
- if (x) for (let e of C) {
132
+ if (C && w) {
133
+ let e = E.map((e) => m(e.value) ? e.value : 1), n = e.reduce((e, t) => Math.min(e, t), Infinity), r = e.reduce((e, t) => Math.max(e, t), -Infinity) - n || 1;
134
+ E.forEach((i, a) => {
135
+ let o = (e[a] - n) / r, c = s.circleMarker([i.lat, i.lng], {
136
+ radius: 10 + o * 22,
137
+ fillColor: h(o),
138
+ fillOpacity: .25 + o * .35,
139
+ stroke: !1
140
+ }).addTo(t);
141
+ c.bindPopup(g(i)), c.on("click", () => P.current(i));
142
+ });
143
+ } else if (C) for (let e of E) {
122
144
  let n = document.createElement("div");
123
145
  Object.assign(n.style, {
124
146
  width: "14px",
@@ -134,51 +156,48 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
134
156
  html: n,
135
157
  iconSize: [14, 14],
136
158
  iconAnchor: [7, 14]
137
- }), i = s.marker([e.lat, e.lng], { icon: r }).addTo(t), a = document.createElement("div"), o = document.createElement("div");
138
- if (o.style.fontWeight = "600", o.textContent = e.popup || e.name || "Location", a.appendChild(o), m(e.value)) {
139
- let t = document.createElement("div");
140
- t.textContent = e.value.toLocaleString(), a.appendChild(t);
141
- }
142
- i.bindPopup(a), i.on("click", () => j.current(e));
159
+ }), i = s.marker([e.lat, e.lng], { icon: r }).addTo(t);
160
+ i.bindPopup(g(e)), i.on("click", () => P.current(e));
143
161
  }
144
162
  t.addTo(e);
145
163
  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
164
+ m: E.map((e) => [e.lat, e.lng]),
165
+ c: D ? [D.lat, D.lng] : null,
166
+ z: O ?? null,
167
+ fc: k ? [k.lat, k.lng] : null,
168
+ fz: A ?? null,
169
+ g: j ? M.size : 0
152
170
  });
153
- if (r !== _.current) if (_.current = r, w) e.setView([w.lat, w.lng], T ?? e.getZoom());
154
- else if (!C.length && n) {
171
+ if (r !== y.current) if (y.current = r, D) e.setView([D.lat, D.lng], O ?? e.getZoom());
172
+ else if (!E.length && n) {
155
173
  let t = n.getBounds();
156
174
  t.isValid() && e.fitBounds(t, {
157
175
  padding: [24, 24],
158
176
  maxZoom: 12
159
177
  });
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]));
178
+ } else if (E.length === 1) e.setView([E[0].lat, E[0].lng], O ?? 8);
179
+ else if (E.length > 1) {
180
+ let t = s.latLngBounds(E.map((e) => [e.lat, e.lng]));
163
181
  e.fitBounds(t, {
164
182
  padding: [32, 32],
165
183
  maxZoom: 12
166
184
  });
167
- } else E ? e.setView([E.lat, E.lng], T ?? D ?? e.getZoom()) : m(T ?? D) && e.setZoom(T ?? D);
185
+ } else k ? e.setView([k.lat, k.lng], O ?? A ?? e.getZoom()) : m(O ?? A) && e.setZoom(O ?? A);
168
186
  let i = window.setTimeout(() => e.invalidateSize(), 0);
169
187
  return () => {
170
188
  window.clearTimeout(i), t.remove();
171
189
  };
172
190
  }, [
173
- C,
174
- w,
175
- T,
176
191
  E,
177
192
  D,
178
- x,
179
193
  O,
180
194
  k,
181
- S
195
+ A,
196
+ C,
197
+ w,
198
+ j,
199
+ M,
200
+ T
182
201
  ]), /* @__PURE__ */ o("div", {
183
202
  className: "relative h-full w-full overflow-hidden rounded-lg bg-bg-muted",
184
203
  children: [
@@ -186,7 +205,7 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
186
205
  ref: f,
187
206
  className: "h-full w-full"
188
207
  }),
189
- v !== "error" && C.length === 0 && !A && /* @__PURE__ */ a("div", {
208
+ b !== "error" && E.length === 0 && !N && /* @__PURE__ */ a("div", {
190
209
  className: "pointer-events-none absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted/70 text-sm text-text-secondary",
191
210
  children: /* @__PURE__ */ o("div", {
192
211
  className: "text-center",
@@ -209,7 +228,7 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
209
228
  }), /* @__PURE__ */ a("p", { children: "No map data" })]
210
229
  })
211
230
  }),
212
- v === "error" && /* @__PURE__ */ a("div", {
231
+ b === "error" && /* @__PURE__ */ a("div", {
213
232
  className: "absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted px-4 text-center text-sm text-text-secondary",
214
233
  children: "The map needs an internet connection and couldn’t load."
215
234
  })
@@ -217,6 +236,6 @@ var _ = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
217
236
  });
218
237
  });
219
238
  //#endregion
220
- export { _ as default };
239
+ export { y as default };
221
240
 
222
241
  //# sourceMappingURL=MapWidget.js.map
@@ -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\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":"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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 * Theme-safe cold→hot ramp for the heat-point layer: normalized intensity\n * (0–1) → info → warning → error status tokens, returned as `var()` refs.\n *\n * We deliberately pass the `var()` reference (not a resolved color): the map\n * uses Leaflet's default SVG renderer, where `fill` participates in the CSS\n * cascade, so the tokens resolve correctly AND stay live across theme toggles\n * (a resolved concrete color would go stale until the next data refresh). This\n * also matches the choropleth layer and avoids any per-marker `getComputedStyle`\n * lookup. (If the map ever opts into `preferCanvas`, pre-resolve these instead.)\n */\nfunction heatColor(norm: number): string {\n if (norm >= 0.66) return 'var(--color-status-error)';\n if (norm >= 0.33) return 'var(--color-status-warning)';\n return 'var(--color-status-info)';\n}\n\n/**\n * Build a marker/region popup as DOM (a bold title + optional formatted value).\n * Uses textContent — never an HTML string — so marker-supplied text can't be\n * interpreted as markup. Shared by the pin and heat-point branches.\n */\nfunction buildMarkerPopup(marker: MapMarker): HTMLDivElement {\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 return popupEl;\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 // Heat-point density mode: render weighted, translucent circles whose overlap\n // accumulates into a heat-like surface (dependency-free — no leaflet.heat).\n const heatMode = config.mapMode === 'heat';\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 && heatMode) {\n // Heat-point layer: translucent weighted circles, sized + colored by value.\n const weights = markers.map((mk) => (isFiniteNumber(mk.value) ? mk.value : 1));\n // reduce, not Math.min(...spread) — spreading a large array as args can\n // blow the call-stack (RangeError) for big datasets.\n const minW = weights.reduce((a, b) => Math.min(a, b), Infinity);\n const maxW = weights.reduce((a, b) => Math.max(a, b), -Infinity);\n const spanW = maxW - minW || 1;\n markers.forEach((marker, i) => {\n const norm = (weights[i] - minW) / spanW;\n const circle = L.circleMarker([marker.lat, marker.lng], {\n radius: 10 + norm * 22,\n fillColor: heatColor(norm),\n // Both radius and opacity ramp with intensity for a truer density look.\n fillOpacity: 0.25 + norm * 0.35,\n stroke: false,\n }).addTo(layer);\n circle.bindPopup(buildMarkerPopup(marker));\n circle.on('click', () => clickRef.current(marker));\n });\n } else 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 m.bindPopup(buildMarkerPopup(marker));\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 }, [\n markers,\n center,\n zoom,\n fallbackCenter,\n fallbackZoom,\n showMarkers,\n heatMode,\n geoJson,\n regionValues,\n regionIdField,\n ]);\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":"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"}
@@ -1,9 +1,16 @@
1
1
  import { CONDITIONAL_ICON_PATHS as e, conditionalStyleToClassName as t, intentToCssVar as n, resolveConditionalStyle as r } from "../utils/conditionalFormat.js";
2
- import { memo as i } from "react";
3
- import { jsx as a, jsxs as o } from "react/jsx-runtime";
2
+ import i from "../metric-card/SparklineChart.js";
3
+ import { memo as a } from "react";
4
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
4
5
  //#region src/bigconsole/components/widgets/table/TableCell.tsx
5
- function s(e, t) {
6
- if (e == null) return /* @__PURE__ */ a("span", {
6
+ function c(e) {
7
+ if (!Array.isArray(e) || e.length === 0) return null;
8
+ let t = [];
9
+ for (let n of e) typeof n == "number" && Number.isFinite(n) ? t.push(n) : n && typeof n == "object" && typeof n.value == "number" && Number.isFinite(n.value) && t.push(n.value);
10
+ return t.length ? t : null;
11
+ }
12
+ function l(e, t) {
13
+ if (e == null) return /* @__PURE__ */ o("span", {
7
14
  className: "text-text-secondary",
8
15
  children: "—"
9
16
  });
@@ -23,7 +30,7 @@ function s(e, t) {
23
30
  return String(e);
24
31
  case "badge": {
25
32
  let n = String(e);
26
- return /* @__PURE__ */ a("span", {
33
+ return /* @__PURE__ */ o("span", {
27
34
  className: `
28
35
  inline-flex items-center
29
36
  px-2 py-0.5
@@ -34,61 +41,74 @@ function s(e, t) {
34
41
  children: n
35
42
  });
36
43
  }
37
- case "link": return /* @__PURE__ */ a("span", {
44
+ case "link": return /* @__PURE__ */ o("span", {
38
45
  className: "text-action-primary-bg hover:underline cursor-pointer",
39
46
  children: String(e)
40
47
  });
41
48
  default: return String(e);
42
49
  }
43
50
  }
44
- var c = i(function({ value: i, column: c, format: l, rowData: u, conditionalRules: d, dataBarMax: f, stickyLeft: p, onClick: m }) {
45
- let h = r(i, d, u), g = t(h), _ = p != null, v = h?.dataBar && typeof i == "number" && f && f > 0 ? Math.max(0, Math.min(100, Math.abs(i) / f * 100)) : null;
46
- return /* @__PURE__ */ o("td", {
51
+ var u = a(function({ value: a, column: u, format: d, rowData: f, conditionalRules: p, dataBarMax: m, stickyLeft: h, cellChart: g, onClick: _ }) {
52
+ let v = r(a, p, f), y = t(v), b = h != null, x = g === "sparkline" ? c(a) : null, S = x ? x.reduce((e, t) => Math.min(e, t), Infinity) : 0, C = x ? x.reduce((e, t) => Math.max(e, t), -Infinity) : 0, w = v?.dataBar && typeof a == "number" && m && m > 0 ? Math.max(0, Math.min(100, Math.abs(a) / m * 100)) : null;
53
+ return /* @__PURE__ */ s("td", {
47
54
  className: `
48
55
  relative px-4 py-3
49
56
  text-sm text-text-primary
50
57
  border-b border-border-default
51
- ${c.align === "center" ? "text-center" : ""}
52
- ${c.align === "right" ? "text-right" : "text-left"}
53
- ${m ? "cursor-pointer hover:text-action-primary-bg" : ""}
54
- ${_ ? "sticky z-10 bg-bg-surface" : ""}
55
- ${g}
58
+ ${u.align === "center" ? "text-center" : ""}
59
+ ${u.align === "right" ? "text-right" : "text-left"}
60
+ ${_ ? "cursor-pointer hover:text-action-primary-bg" : ""}
61
+ ${b ? "sticky z-10 bg-bg-surface" : ""}
62
+ ${y}
56
63
  `,
57
- style: _ ? {
58
- left: p,
59
- width: c.width
64
+ style: b ? {
65
+ left: h,
66
+ width: u.width
60
67
  } : void 0,
61
- onClick: m,
62
- role: m ? "button" : void 0,
63
- tabIndex: m ? 0 : void 0,
68
+ onClick: _,
69
+ role: _ ? "button" : void 0,
70
+ tabIndex: _ ? 0 : void 0,
64
71
  onKeyDown: (e) => {
65
- m && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), m());
72
+ _ && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), _());
66
73
  },
67
- children: [v != null && /* @__PURE__ */ a("span", {
74
+ children: [w != null && /* @__PURE__ */ o("span", {
68
75
  "aria-hidden": "true",
69
76
  className: "absolute inset-y-1 left-1 rounded-sm opacity-20 pointer-events-none",
70
77
  style: {
71
- width: `calc(${v}% - 0.5rem)`,
72
- backgroundColor: n(h?.dataBarColor ?? "info")
78
+ width: `calc(${w}% - 0.5rem)`,
79
+ backgroundColor: n(v?.dataBarColor ?? "info")
73
80
  }
74
- }), /* @__PURE__ */ o("span", {
81
+ }), x && x.length >= 2 ? /* @__PURE__ */ o("span", {
82
+ className: "relative inline-flex items-center",
83
+ role: "img",
84
+ "aria-label": `Sparkline, ${x.length} points, ${S.toLocaleString()} to ${C.toLocaleString()}`,
85
+ children: /* @__PURE__ */ o(i, {
86
+ data: x.map((e) => ({ value: e })),
87
+ width: 96,
88
+ height: 28,
89
+ color: "primary"
90
+ })
91
+ }) : x && x.length === 1 ? /* @__PURE__ */ o("span", {
92
+ className: "relative inline-flex items-center",
93
+ children: l(x[0], d)
94
+ }) : /* @__PURE__ */ s("span", {
75
95
  className: "relative inline-flex items-center gap-1.5",
76
- children: [h?.icon && /* @__PURE__ */ a("svg", {
96
+ children: [v?.icon && /* @__PURE__ */ o("svg", {
77
97
  className: "w-3.5 h-3.5 shrink-0",
78
98
  fill: "none",
79
99
  stroke: "currentColor",
80
100
  viewBox: "0 0 24 24",
81
- children: /* @__PURE__ */ a("path", {
101
+ children: /* @__PURE__ */ o("path", {
82
102
  strokeLinecap: "round",
83
103
  strokeLinejoin: "round",
84
104
  strokeWidth: 2,
85
- d: e[h.icon]
105
+ d: e[v.icon]
86
106
  })
87
- }), s(i, l)]
107
+ }), l(a, d)]
88
108
  })]
89
109
  });
90
110
  });
91
111
  //#endregion
92
- export { c as default };
112
+ export { u as default };
93
113
 
94
114
  //# sourceMappingURL=TableCell.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TableCell.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/table/TableCell.tsx"],"sourcesContent":["/**\n * TableCell Component\n *\n * Table cell with formatting and rendering options.\n */\n\nimport { type FC, memo, type ReactNode } from 'react';\nimport type { ColumnDefinition } from './TableHeader';\nimport {\n type ConditionalRule,\n resolveConditionalStyle,\n conditionalStyleToClassName,\n intentToCssVar,\n CONDITIONAL_ICON_PATHS,\n} from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type CellFormat = 'text' | 'number' | 'currency' | 'percentage' | 'date' | 'badge' | 'link';\n\nexport interface CellFormatConfig {\n /** Format type */\n type: CellFormat;\n /** Currency code for currency format */\n currency?: string;\n /** Date format options */\n dateOptions?: Intl.DateTimeFormatOptions;\n /** Badge color mapping (value -> color) */\n badgeColors?: Record<string, string>;\n /** Link URL pattern (use {value} as placeholder) */\n linkPattern?: string;\n}\n\nexport interface TableCellProps {\n /** Cell value */\n value: unknown;\n /** Column definition */\n column: ColumnDefinition;\n /** Format configuration */\n format?: CellFormatConfig;\n /** Row data for link patterns and conditional rule field lookups */\n rowData?: Record<string, unknown>;\n /** Conditional formatting rules to evaluate against this cell */\n conditionalRules?: ConditionalRule[];\n /** Max absolute value in this column, used to scale in-cell data bars */\n dataBarMax?: number;\n /** When set, freeze this cell sticky-left at the given px offset */\n stickyLeft?: number;\n /** Click handler */\n onClick?: () => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: unknown, format?: CellFormatConfig): ReactNode {\n if (value === null || value === undefined) {\n return <span className=\"text-text-secondary\">—</span>;\n }\n\n const formatType = format?.type || 'text';\n\n switch (formatType) {\n case 'number':\n return typeof value === 'number' ? new Intl.NumberFormat('en-US').format(value) : String(value);\n\n case 'currency':\n return typeof value === 'number'\n ? new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: format?.currency || 'USD',\n }).format(value)\n : String(value);\n\n case 'percentage':\n return typeof value === 'number' ? `${(value * 100).toFixed(1)}%` : String(value);\n\n case 'date':\n if (value instanceof Date) {\n return new Intl.DateTimeFormat('en-US', format?.dateOptions || { dateStyle: 'medium' }).format(value);\n }\n if (typeof value === 'string' || typeof value === 'number') {\n const date = new Date(value);\n if (!isNaN(date.getTime())) {\n return new Intl.DateTimeFormat('en-US', format?.dateOptions || { dateStyle: 'medium' }).format(date);\n }\n }\n return String(value);\n\n case 'badge': {\n const stringValue = String(value);\n const badgeColor = format?.badgeColors?.[stringValue] || 'bg-bg-muted text-text-primary';\n return (\n <span\n className={`\n inline-flex items-center\n px-2 py-0.5\n text-xs font-medium\n rounded-full\n ${badgeColor}\n `}\n >\n {stringValue}\n </span>\n );\n }\n\n case 'link':\n return <span className=\"text-action-primary-bg hover:underline cursor-pointer\">{String(value)}</span>;\n\n case 'text':\n default:\n return String(value);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TableCell: FC<TableCellProps> = memo(function TableCell({\n value,\n column,\n format,\n rowData,\n conditionalRules,\n dataBarMax,\n stickyLeft,\n onClick,\n}) {\n const cf = resolveConditionalStyle(value, conditionalRules, rowData);\n const cfClass = conditionalStyleToClassName(cf);\n const isFrozen = stickyLeft != null;\n\n // In-cell data bar width (0–100%) scaled against the column max.\n const dataBarWidth =\n cf?.dataBar && typeof value === 'number' && dataBarMax && dataBarMax > 0\n ? Math.max(0, Math.min(100, (Math.abs(value) / dataBarMax) * 100))\n : null;\n\n return (\n <td\n className={`\n relative px-4 py-3\n text-sm text-text-primary\n border-b border-border-default\n ${column.align === 'center' ? 'text-center' : ''}\n ${column.align === 'right' ? 'text-right' : 'text-left'}\n ${onClick ? 'cursor-pointer hover:text-action-primary-bg' : ''}\n ${isFrozen ? 'sticky z-10 bg-bg-surface' : ''}\n ${cfClass}\n `}\n style={isFrozen ? { left: stickyLeft, width: column.width } : undefined}\n onClick={onClick}\n role={onClick ? 'button' : undefined}\n tabIndex={onClick ? 0 : undefined}\n onKeyDown={(e) => {\n if (onClick && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n onClick();\n }\n }}\n >\n {dataBarWidth != null && (\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-y-1 left-1 rounded-sm opacity-20 pointer-events-none\"\n style={{\n width: `calc(${dataBarWidth}% - 0.5rem)`,\n backgroundColor: intentToCssVar(cf?.dataBarColor ?? 'info'),\n }}\n />\n )}\n <span className=\"relative inline-flex items-center gap-1.5\">\n {cf?.icon && (\n <svg className=\"w-3.5 h-3.5 shrink-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d={CONDITIONAL_ICON_PATHS[cf.icon]} />\n </svg>\n )}\n {formatValue(value, format)}\n </span>\n </td>\n );\n});\n\nexport default TableCell;\n"],"mappings":";;;;AA0DA,SAAS,EAAY,GAAgB,GAAsC;AACzE,KAAI,KAAU,KACZ,QAAO,kBAAC,QAAD;EAAM,WAAU;YAAsB;EAAQ,CAAA;AAKvD,SAFmB,GAAQ,QAAQ,QAEnC;EACE,KAAK,SACH,QAAO,OAAO,KAAU,WAAW,IAAI,KAAK,aAAa,QAAQ,CAAC,OAAO,EAAM,GAAG,OAAO,EAAM;EAEjG,KAAK,WACH,QAAO,OAAO,KAAU,WACpB,IAAI,KAAK,aAAa,SAAS;GAC7B,OAAO;GACP,UAAU,GAAQ,YAAY;GAC/B,CAAC,CAAC,OAAO,EAAM,GAChB,OAAO,EAAM;EAEnB,KAAK,aACH,QAAO,OAAO,KAAU,WAAW,IAAI,IAAQ,KAAK,QAAQ,EAAE,CAAC,KAAK,OAAO,EAAM;EAEnF,KAAK;AACH,OAAI,aAAiB,KACnB,QAAO,IAAI,KAAK,eAAe,SAAS,GAAQ,eAAe,EAAE,WAAW,UAAU,CAAC,CAAC,OAAO,EAAM;AAEvG,OAAI,OAAO,KAAU,YAAY,OAAO,KAAU,UAAU;IAC1D,IAAM,IAAO,IAAI,KAAK,EAAM;AAC5B,QAAI,CAAC,MAAM,EAAK,SAAS,CAAC,CACxB,QAAO,IAAI,KAAK,eAAe,SAAS,GAAQ,eAAe,EAAE,WAAW,UAAU,CAAC,CAAC,OAAO,EAAK;;AAGxG,UAAO,OAAO,EAAM;EAEtB,KAAK,SAAS;GACZ,IAAM,IAAc,OAAO,EAAM;AAEjC,UACE,kBAAC,QAAD;IACE,WAAW;;;;;cAHI,GAAQ,cAAc,MAAgB,gCAQtC;;cAGd;IACI,CAAA;;EAIX,KAAK,OACH,QAAO,kBAAC,QAAD;GAAM,WAAU;aAAyD,OAAO,EAAM;GAAQ,CAAA;EAGvG,QACE,QAAO,OAAO,EAAM;;;AAQ1B,IAAa,IAAgC,EAAK,SAAmB,EACnE,UACA,WACA,WACA,YACA,qBACA,eACA,eACA,cACC;CACD,IAAM,IAAK,EAAwB,GAAO,GAAkB,EAAQ,EAC9D,IAAU,EAA4B,EAAG,EACzC,IAAW,KAAc,MAGzB,IACJ,GAAI,WAAW,OAAO,KAAU,YAAY,KAAc,IAAa,IACnE,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,KAAK,IAAI,EAAM,GAAG,IAAc,IAAI,CAAC,GAChE;AAEN,QACE,kBAAC,MAAD;EACE,WAAW;;;;UAIP,EAAO,UAAU,WAAW,gBAAgB,GAAG;UAC/C,EAAO,UAAU,UAAU,eAAe,YAAY;UACtD,IAAU,gDAAgD,GAAG;UAC7D,IAAW,8BAA8B,GAAG;UAC5C,EAAQ;;EAEZ,OAAO,IAAW;GAAE,MAAM;GAAY,OAAO,EAAO;GAAO,GAAG,KAAA;EACrD;EACT,MAAM,IAAU,WAAW,KAAA;EAC3B,UAAU,IAAU,IAAI,KAAA;EACxB,YAAY,MAAM;AAChB,GAAI,MAAY,EAAE,QAAQ,WAAW,EAAE,QAAQ,SAC7C,EAAE,gBAAgB,EAClB,GAAS;;YAlBf,CAsBG,KAAgB,QACf,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;GACV,OAAO;IACL,OAAO,QAAQ,EAAa;IAC5B,iBAAiB,EAAe,GAAI,gBAAgB,OAAO;IAC5D;GACD,CAAA,EAEJ,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACG,GAAI,QACH,kBAAC,OAAD;IAAK,WAAU;IAAuB,MAAK;IAAO,QAAO;IAAe,SAAQ;cAC9E,kBAAC,QAAD;KAAM,eAAc;KAAQ,gBAAe;KAAQ,aAAa;KAAG,GAAG,EAAuB,EAAG;KAAS,CAAA;IACrG,CAAA,EAEP,EAAY,GAAO,EAAO,CACtB;KACJ;;EAEP"}
1
+ {"version":3,"file":"TableCell.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/table/TableCell.tsx"],"sourcesContent":["/**\n * TableCell Component\n *\n * Table cell with formatting and rendering options.\n */\n\nimport { type FC, memo, type ReactNode } from 'react';\nimport type { ColumnDefinition } from './TableHeader';\nimport {\n type ConditionalRule,\n resolveConditionalStyle,\n conditionalStyleToClassName,\n intentToCssVar,\n CONDITIONAL_ICON_PATHS,\n} from '../utils/conditionalFormat';\nimport { SparklineChart } from '../metric-card/SparklineChart';\n\n/** In-cell mini-chart kinds. */\nexport type CellChart = 'sparkline';\n\n/**\n * Extract finite numeric values from a sparkline cell value (an array of raw\n * numbers or `{value}` objects). Non-finite values (NaN/Infinity) are rejected\n * in both forms. Returns null when the value isn't a usable numeric array — so\n * the caller can decide how to render: a chart (≥2 points), a single value\n * (1 point), or fall through to the default renderer.\n */\nfunction extractSparklineValues(value: unknown): number[] | null {\n if (!Array.isArray(value) || value.length === 0) return null;\n const nums: number[] = [];\n for (const item of value) {\n if (typeof item === 'number' && Number.isFinite(item)) {\n nums.push(item);\n } else if (\n item &&\n typeof item === 'object' &&\n typeof (item as { value?: unknown }).value === 'number' &&\n Number.isFinite((item as { value: number }).value)\n ) {\n nums.push((item as { value: number }).value);\n }\n }\n return nums.length ? nums : null;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type CellFormat = 'text' | 'number' | 'currency' | 'percentage' | 'date' | 'badge' | 'link';\n\nexport interface CellFormatConfig {\n /** Format type */\n type: CellFormat;\n /** Currency code for currency format */\n currency?: string;\n /** Date format options */\n dateOptions?: Intl.DateTimeFormatOptions;\n /** Badge color mapping (value -> color) */\n badgeColors?: Record<string, string>;\n /** Link URL pattern (use {value} as placeholder) */\n linkPattern?: string;\n}\n\nexport interface TableCellProps {\n /** Cell value */\n value: unknown;\n /** Column definition */\n column: ColumnDefinition;\n /** Format configuration */\n format?: CellFormatConfig;\n /** Row data for link patterns and conditional rule field lookups */\n rowData?: Record<string, unknown>;\n /** Conditional formatting rules to evaluate against this cell */\n conditionalRules?: ConditionalRule[];\n /** Max absolute value in this column, used to scale in-cell data bars */\n dataBarMax?: number;\n /** When set, freeze this cell sticky-left at the given px offset */\n stickyLeft?: number;\n /** Render the cell as an in-cell mini chart (e.g. sparkline) instead of a value */\n cellChart?: CellChart;\n /** Click handler */\n onClick?: () => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: unknown, format?: CellFormatConfig): ReactNode {\n if (value === null || value === undefined) {\n return <span className=\"text-text-secondary\">—</span>;\n }\n\n const formatType = format?.type || 'text';\n\n switch (formatType) {\n case 'number':\n return typeof value === 'number' ? new Intl.NumberFormat('en-US').format(value) : String(value);\n\n case 'currency':\n return typeof value === 'number'\n ? new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: format?.currency || 'USD',\n }).format(value)\n : String(value);\n\n case 'percentage':\n return typeof value === 'number' ? `${(value * 100).toFixed(1)}%` : String(value);\n\n case 'date':\n if (value instanceof Date) {\n return new Intl.DateTimeFormat('en-US', format?.dateOptions || { dateStyle: 'medium' }).format(value);\n }\n if (typeof value === 'string' || typeof value === 'number') {\n const date = new Date(value);\n if (!isNaN(date.getTime())) {\n return new Intl.DateTimeFormat('en-US', format?.dateOptions || { dateStyle: 'medium' }).format(date);\n }\n }\n return String(value);\n\n case 'badge': {\n const stringValue = String(value);\n const badgeColor = format?.badgeColors?.[stringValue] || 'bg-bg-muted text-text-primary';\n return (\n <span\n className={`\n inline-flex items-center\n px-2 py-0.5\n text-xs font-medium\n rounded-full\n ${badgeColor}\n `}\n >\n {stringValue}\n </span>\n );\n }\n\n case 'link':\n return <span className=\"text-action-primary-bg hover:underline cursor-pointer\">{String(value)}</span>;\n\n case 'text':\n default:\n return String(value);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TableCell: FC<TableCellProps> = memo(function TableCell({\n value,\n column,\n format,\n rowData,\n conditionalRules,\n dataBarMax,\n stickyLeft,\n cellChart,\n onClick,\n}) {\n const cf = resolveConditionalStyle(value, conditionalRules, rowData);\n const cfClass = conditionalStyleToClassName(cf);\n const isFrozen = stickyLeft != null;\n // In-cell sparkline: only when the column opts in and the value is numeric.\n const sparkValues = cellChart === 'sparkline' ? extractSparklineValues(value) : null;\n // Range for the sparkline's accessible label (reduce, not spread → no call-stack risk).\n const sparkMin = sparkValues ? sparkValues.reduce((a, b) => Math.min(a, b), Infinity) : 0;\n const sparkMax = sparkValues ? sparkValues.reduce((a, b) => Math.max(a, b), -Infinity) : 0;\n\n // In-cell data bar width (0–100%) scaled against the column max.\n const dataBarWidth =\n cf?.dataBar && typeof value === 'number' && dataBarMax && dataBarMax > 0\n ? Math.max(0, Math.min(100, (Math.abs(value) / dataBarMax) * 100))\n : null;\n\n return (\n <td\n className={`\n relative px-4 py-3\n text-sm text-text-primary\n border-b border-border-default\n ${column.align === 'center' ? 'text-center' : ''}\n ${column.align === 'right' ? 'text-right' : 'text-left'}\n ${onClick ? 'cursor-pointer hover:text-action-primary-bg' : ''}\n ${isFrozen ? 'sticky z-10 bg-bg-surface' : ''}\n ${cfClass}\n `}\n style={isFrozen ? { left: stickyLeft, width: column.width } : undefined}\n onClick={onClick}\n role={onClick ? 'button' : undefined}\n tabIndex={onClick ? 0 : undefined}\n onKeyDown={(e) => {\n if (onClick && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n onClick();\n }\n }}\n >\n {dataBarWidth != null && (\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-y-1 left-1 rounded-sm opacity-20 pointer-events-none\"\n style={{\n width: `calc(${dataBarWidth}% - 0.5rem)`,\n backgroundColor: intentToCssVar(cf?.dataBarColor ?? 'info'),\n }}\n />\n )}\n {sparkValues && sparkValues.length >= 2 ? (\n // ≥2 points → draw the sparkline (SparklineChart needs >1 to avoid a\n // divide-by-zero). Labelled for screen readers since the SVG is decorative.\n <span\n className=\"relative inline-flex items-center\"\n role=\"img\"\n aria-label={`Sparkline, ${sparkValues.length} points, ${sparkMin.toLocaleString()} to ${sparkMax.toLocaleString()}`}\n >\n <SparklineChart data={sparkValues.map((v) => ({ value: v }))} width={96} height={28} color=\"primary\" />\n </span>\n ) : sparkValues && sparkValues.length === 1 ? (\n // A single numeric point can't be charted — render the value itself\n // (not the raw array, which would stringify to \"[object Object]\").\n <span className=\"relative inline-flex items-center\">{formatValue(sparkValues[0], format)}</span>\n ) : (\n <span className=\"relative inline-flex items-center gap-1.5\">\n {cf?.icon && (\n <svg className=\"w-3.5 h-3.5 shrink-0\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d={CONDITIONAL_ICON_PATHS[cf.icon]} />\n </svg>\n )}\n {formatValue(value, format)}\n </span>\n )}\n </td>\n );\n});\n\nexport default TableCell;\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import i from "./TablePagination.js";
6
6
  import { memo as a, useCallback as o, useEffect as s, useMemo as c, useRef as l, useState as u } from "react";
7
7
  import { jsx as d, jsxs as f } from "react/jsx-runtime";
8
8
  //#region src/bigconsole/components/widgets/table/TableWidget.tsx
9
- function p(e, t) {
9
+ function ee(e, t) {
10
10
  return !t.column || !t.direction ? e : [...e].sort((e, n) => {
11
11
  let r = e[t.column], i = n[t.column];
12
12
  if (r == null && i == null) return 0;
@@ -16,7 +16,7 @@ function p(e, t) {
16
16
  return a = typeof r == "number" && typeof i == "number" ? r - i : String(r).localeCompare(String(i)), t.direction === "asc" ? a : -a;
17
17
  });
18
18
  }
19
- function m(e, t, n) {
19
+ function p(e, t, n) {
20
20
  if (!t.trim()) return e;
21
21
  let r = t.toLowerCase();
22
22
  return e.filter((e) => n.some((t) => {
@@ -24,7 +24,7 @@ function m(e, t, n) {
24
24
  return n != null && String(n).toLowerCase().includes(r);
25
25
  }));
26
26
  }
27
- function h(e, t, n) {
27
+ function m(e, t, n) {
28
28
  switch (t) {
29
29
  case "currency": return new Intl.NumberFormat("en-US", {
30
30
  style: "currency",
@@ -35,10 +35,10 @@ function h(e, t, n) {
35
35
  default: return Number.isInteger(e) ? String(e) : e.toFixed(2);
36
36
  }
37
37
  }
38
- var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
39
- let y = a.config, b = c(() => {
40
- if (Array.isArray(g)) return g;
41
- if (g && typeof g == "object") {
38
+ var h = a(function({ widget: a, data: h = [], onRowClick: g, onDrilldown: _ }) {
39
+ let v = a.config, y = c(() => {
40
+ if (Array.isArray(h)) return h;
41
+ if (h && typeof h == "object") {
42
42
  for (let e of [
43
43
  "data",
44
44
  "items",
@@ -49,20 +49,20 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
49
49
  "sales",
50
50
  "entries",
51
51
  "values"
52
- ]) if (Array.isArray(g[e])) return g[e];
53
- let e = Object.values(g);
52
+ ]) if (Array.isArray(h[e])) return h[e];
53
+ let e = Object.values(h);
54
54
  for (let t of e) if (Array.isArray(t)) return t;
55
- return [g];
55
+ return [h];
56
56
  }
57
57
  return [];
58
- }, [g]), { handleDataClick: x, enabled: S } = t(a.id, y?.crossFilterMappings), { clearAllCrossFilters: C, hasCrossFilters: w } = e(), [T, E] = u({
59
- column: y?.defaultSortColumn || null,
60
- direction: y?.defaultSortDirection || null
61
- }), [D, O] = u(""), [k, A] = u({
58
+ }, [h]), { handleDataClick: b, enabled: x } = t(a.id, v?.crossFilterMappings), { clearAllCrossFilters: S, hasCrossFilters: C } = e(), [w, T] = u({
59
+ column: v?.defaultSortColumn || null,
60
+ direction: v?.defaultSortDirection || null
61
+ }), [E, D] = u(""), [O, k] = u({
62
62
  page: 0,
63
- pageSize: y?.pageSize || 10,
63
+ pageSize: v?.pageSize || 10,
64
64
  totalItems: 0
65
- }), j = c(() => y?.columns?.length ? y.columns.map((e) => {
65
+ }), A = c(() => v?.columns?.length ? v.columns.map((e) => {
66
66
  if (typeof e == "string") return {
67
67
  key: e,
68
68
  header: e.charAt(0).toUpperCase() + e.slice(1).replace(/_/g, " "),
@@ -76,39 +76,45 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
76
76
  width: e.width == null ? void 0 : String(e.width),
77
77
  align: e.align
78
78
  };
79
- }) : b.length > 0 ? Object.keys(b[0]).map((e) => ({
79
+ }) : y.length > 0 ? Object.keys(y[0]).map((e) => ({
80
80
  key: e,
81
81
  header: e.charAt(0).toUpperCase() + e.slice(1).replace(/_/g, " "),
82
82
  sortable: !0
83
- })) : [], [y?.columns, b]), M = c(() => {
83
+ })) : [], [v?.columns, y]), j = c(() => {
84
84
  let e = {};
85
- return y?.columns?.forEach((t) => {
85
+ return v?.columns?.forEach((t) => {
86
86
  t.format && (t.key || t.field) && (e[t.key || t.field || ""] = { type: t.format });
87
87
  }), e;
88
- }, [y?.columns]), N = y?.conditionalRules, P = c(() => {
88
+ }, [v?.columns]), M = c(() => {
89
+ let e = {};
90
+ return v?.columns?.forEach((t) => {
91
+ let n = t.key || t.field;
92
+ n && t.cellChart && (e[n] = t.cellChart);
93
+ }), e;
94
+ }, [v?.columns]), N = v?.conditionalRules, P = c(() => {
89
95
  let e = {};
90
- return y?.columns?.forEach((t) => {
96
+ return v?.columns?.forEach((t) => {
91
97
  let n = t.key || t.field;
92
98
  n && t.conditionalRules?.length && (e[n] = t.conditionalRules);
93
99
  }), e;
94
- }, [y?.columns]), F = c(() => {
100
+ }, [v?.columns]), F = c(() => {
95
101
  let e = {};
96
- return y?.columns?.forEach((t) => {
102
+ return v?.columns?.forEach((t) => {
97
103
  let n = t.key || t.field;
98
104
  n && t.total && t.total !== "none" && (e[n] = t.total);
99
105
  }), e;
100
- }, [y?.columns]), I = y?.showTotals === !0 || Object.keys(F).length > 0, L = c(() => {
101
- let e = b;
102
- return y?.showSearch !== !1 && (e = m(e, D, j)), e = p(e, T), e;
106
+ }, [v?.columns]), I = v?.showTotals === !0 || Object.keys(F).length > 0, L = c(() => {
107
+ let e = y;
108
+ return v?.showSearch !== !1 && (e = p(e, E, A)), e = ee(e, w), e;
103
109
  }, [
104
- b,
105
- D,
106
- T,
107
- j,
108
- y?.showSearch
110
+ y,
111
+ E,
112
+ w,
113
+ A,
114
+ v?.showSearch
109
115
  ]), R = c(() => {
110
116
  let e = {};
111
- for (let t of j) {
117
+ for (let t of A) {
112
118
  let n = 0;
113
119
  for (let e of L) {
114
120
  let r = e[t.key];
@@ -117,10 +123,10 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
117
123
  e[t.key] = n;
118
124
  }
119
125
  return e;
120
- }, [j, L]), z = c(() => {
126
+ }, [A, L]), z = c(() => {
121
127
  if (!I) return {};
122
128
  let e = {};
123
- for (let t of j) {
129
+ for (let t of A) {
124
130
  let n = F[t.key];
125
131
  if (!n) {
126
132
  e[t.key] = null;
@@ -132,25 +138,25 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
132
138
  return e;
133
139
  }, [
134
140
  I,
135
- j,
141
+ A,
136
142
  F,
137
143
  L
138
144
  ]);
139
145
  c(() => {
140
- A((e) => ({
146
+ k((e) => ({
141
147
  ...e,
142
148
  totalItems: L.length
143
149
  }));
144
150
  }, [L.length]);
145
151
  let B = c(() => {
146
- if (y?.showPagination === !1) return L;
147
- let e = k.page * k.pageSize;
148
- return L.slice(e, e + k.pageSize);
152
+ if (v?.showPagination === !1) return L;
153
+ let e = O.page * O.pageSize;
154
+ return L.slice(e, e + O.pageSize);
149
155
  }, [
150
156
  L,
151
- k,
152
- y?.showPagination
153
- ]), V = y?.virtualized === !0, H = y?.rowHeight && y.rowHeight > 0 ? y.rowHeight : 44, U = l(null), [W, G] = u(0), [K, q] = u(0);
157
+ O,
158
+ v?.showPagination
159
+ ]), V = v?.virtualized === !0, H = v?.rowHeight && v.rowHeight > 0 ? v.rowHeight : 44, U = l(null), [W, G] = u(0), [K, q] = u(0);
154
160
  s(() => {
155
161
  if (!V) return;
156
162
  let e = U.current;
@@ -174,14 +180,14 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
174
180
  K,
175
181
  W,
176
182
  H
177
- ]), Y = V && J ? J.rows : B, X = V && J ? J.startIndex : 0, Z = y?.frozenColumns && y.frozenColumns > 0 ? y.frozenColumns : 0, { displayColumns: Q, frozenLefts: $ } = c(() => {
183
+ ]), Y = V && J ? J.rows : B, X = V && J ? J.startIndex : 0, Z = v?.frozenColumns && v.frozenColumns > 0 ? v.frozenColumns : 0, { displayColumns: Q, frozenLefts: $ } = c(() => {
178
184
  if (Z <= 0) return {
179
- displayColumns: j,
185
+ displayColumns: A,
180
186
  frozenLefts: {}
181
187
  };
182
188
  let e = {}, t = 0;
183
189
  return {
184
- displayColumns: j.map((n, r) => {
190
+ displayColumns: A.map((n, r) => {
185
191
  if (r < Z) {
186
192
  let r = n.width && parseInt(n.width, 10) || 160;
187
193
  return e[n.key] = t, t += r, {
@@ -193,8 +199,8 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
193
199
  }),
194
200
  frozenLefts: e
195
201
  };
196
- }, [j, Z]), ee = o((e) => {
197
- E((t) => {
202
+ }, [A, Z]), te = o((e) => {
203
+ T((t) => {
198
204
  if (t.column !== e) return {
199
205
  column: e,
200
206
  direction: "asc"
@@ -205,35 +211,35 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
205
211
  direction: n
206
212
  };
207
213
  });
208
- }, []), te = o((e) => {
209
- A((t) => ({
214
+ }, []), ne = o((e) => {
215
+ k((t) => ({
210
216
  ...t,
211
217
  page: e
212
218
  }));
213
- }, []), ne = o((e) => {
214
- A((t) => ({
219
+ }, []), re = o((e) => {
220
+ k((t) => ({
215
221
  ...t,
216
222
  pageSize: e,
217
223
  page: 0
218
224
  }));
219
- }, []), re = o((e, t) => {
220
- let n = y?.crossFilterMappings?.length || y?.enableCrossFilter;
221
- if (S && n) {
222
- let t = j[0]?.key;
223
- x(e, t ? String(e[t]) : void 0);
225
+ }, []), ie = o((e, t) => {
226
+ let n = v?.crossFilterMappings?.length || v?.enableCrossFilter;
227
+ if (x && n) {
228
+ let t = A[0]?.key;
229
+ b(e, t ? String(e[t]) : void 0);
224
230
  }
225
- v ? v(e) : _ && a.drilldowns?.length && _(e, t);
231
+ _ ? _(e) : g && a.drilldowns?.length && g(e, t);
226
232
  }, [
233
+ g,
227
234
  _,
228
- v,
229
235
  a.drilldowns,
230
- S,
231
236
  x,
232
- j,
233
- y?.crossFilterMappings,
234
- y?.enableCrossFilter
235
- ]), ie = o(() => {
236
- let e = [j.map((e) => e.header).join(","), ...L.map((e) => j.map((t) => {
237
+ b,
238
+ A,
239
+ v?.crossFilterMappings,
240
+ v?.enableCrossFilter
241
+ ]), ae = o(() => {
242
+ let e = [A.map((e) => e.header).join(","), ...L.map((e) => A.map((t) => {
237
243
  let n = e[t.key];
238
244
  if (n == null) return "";
239
245
  let r = String(n);
@@ -241,19 +247,19 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
241
247
  }).join(","))].join("\n"), t = new Blob([e], { type: "text/csv" }), n = URL.createObjectURL(t), r = document.createElement("a");
242
248
  r.href = n, r.download = `${a.title || "table"}-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.csv`, r.click(), URL.revokeObjectURL(n);
243
249
  }, [
244
- j,
250
+ A,
245
251
  L,
246
252
  a.title
247
- ]), ae = y?.crossFilterMappings?.length || y?.enableCrossFilter, oe = !!(v || _ && a.drilldowns?.length || ae);
253
+ ]), oe = v?.crossFilterMappings?.length || v?.enableCrossFilter, se = !!(_ || g && a.drilldowns?.length || oe);
248
254
  return /* @__PURE__ */ f("div", {
249
255
  className: "flex flex-col h-full",
250
256
  children: [
251
- w && /* @__PURE__ */ d("div", {
257
+ C && /* @__PURE__ */ d("div", {
252
258
  className: "flex items-center gap-2 px-3 py-2 border-b border-border-default",
253
259
  children: /* @__PURE__ */ f("button", {
254
260
  type: "button",
255
261
  onClick: (e) => {
256
- e.stopPropagation(), C();
262
+ e.stopPropagation(), S();
257
263
  },
258
264
  className: "\n inline-flex items-center gap-1.5\n px-2.5 py-1 rounded-md\n text-xs font-medium\n bg-action-primary-bg/10 text-action-primary-text\n hover:bg-action-primary-bg/20\n transition-colors\n ",
259
265
  children: [/* @__PURE__ */ d("svg", {
@@ -270,9 +276,9 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
270
276
  }), "Clear Filter"]
271
277
  })
272
278
  }),
273
- (y?.showSearch !== !1 || y?.showExport !== !1) && /* @__PURE__ */ f("div", {
279
+ (v?.showSearch !== !1 || v?.showExport !== !1) && /* @__PURE__ */ f("div", {
274
280
  className: "flex items-center justify-between p-3 border-b border-border-default",
275
- children: [y?.showSearch !== !1 && /* @__PURE__ */ f("div", {
281
+ children: [v?.showSearch !== !1 && /* @__PURE__ */ f("div", {
276
282
  className: "relative flex items-center",
277
283
  children: [/* @__PURE__ */ d("svg", {
278
284
  className: "absolute left-3 w-4 h-4 text-text-secondary pointer-events-none",
@@ -288,13 +294,13 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
288
294
  }), /* @__PURE__ */ d("input", {
289
295
  type: "text",
290
296
  placeholder: "Search products...",
291
- value: D,
292
- onChange: (e) => O(e.target.value),
297
+ value: E,
298
+ onChange: (e) => D(e.target.value),
293
299
  className: "\n pl-10 pr-4 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg focus:border-action-primary-bg\n w-48 sm:w-64\n placeholder:text-text-tertiary\n "
294
300
  })]
295
- }), y?.showExport !== !1 && /* @__PURE__ */ f("button", {
301
+ }), v?.showExport !== !1 && /* @__PURE__ */ f("button", {
296
302
  type: "button",
297
- onClick: ie,
303
+ onClick: ae,
298
304
  className: "\n inline-flex items-center gap-2\n px-3 py-2\n text-sm font-medium\n text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n hover:bg-bg-muted\n transition-colors\n ",
299
305
  children: [/* @__PURE__ */ d("svg", {
300
306
  className: "w-4 h-4",
@@ -319,8 +325,8 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
319
325
  children: [
320
326
  /* @__PURE__ */ d(n, {
321
327
  columns: Q,
322
- sortState: T,
323
- onSort: ee,
328
+ sortState: w,
329
+ onSort: te,
324
330
  frozenLefts: Z > 0 ? $ : void 0
325
331
  }),
326
332
  /* @__PURE__ */ f("tbody", { children: [
@@ -339,19 +345,20 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
339
345
  let n = X + t;
340
346
  return /* @__PURE__ */ d("tr", {
341
347
  className: `
342
- ${oe ? "cursor-pointer hover:bg-bg-muted/50" : ""}
348
+ ${se ? "cursor-pointer hover:bg-bg-muted/50" : ""}
343
349
  ${n % 2 == 0 ? "bg-bg-surface" : "bg-bg-muted/30"}
344
350
  `,
345
351
  style: V ? { height: H } : void 0,
346
- onClick: () => re(e, n),
352
+ onClick: () => ie(e, n),
347
353
  children: Q.map((t) => /* @__PURE__ */ d(r, {
348
354
  value: e[t.key],
349
355
  column: t,
350
- format: M[t.key],
356
+ format: j[t.key],
351
357
  rowData: e,
352
358
  conditionalRules: P[t.key] ?? N,
353
359
  dataBarMax: R[t.key],
354
- stickyLeft: $[t.key]
360
+ stickyLeft: $[t.key],
361
+ cellChart: M[t.key]
355
362
  }, t.key))
356
363
  }, n);
357
364
  }),
@@ -370,7 +377,7 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
370
377
  I && L.length > 0 && /* @__PURE__ */ d("tfoot", { children: /* @__PURE__ */ d("tr", {
371
378
  className: "bg-bg-muted/50 font-semibold border-t-2 border-border-default",
372
379
  children: Q.map((e, t) => {
373
- let n = z[e.key], r = M[e.key], i = $[e.key];
380
+ let n = z[e.key], r = j[e.key], i = $[e.key];
374
381
  return /* @__PURE__ */ d("td", {
375
382
  className: `
376
383
  px-4 py-3 text-sm text-text-primary
@@ -379,28 +386,28 @@ var g = a(function({ widget: a, data: g = [], onRowClick: _, onDrilldown: v }) {
379
386
  ${i == null ? "" : "sticky z-10 bg-bg-muted"}
380
387
  `,
381
388
  style: i == null ? void 0 : { left: i },
382
- children: n == null ? t === 0 ? "Total" : "" : h(n, r?.type, r?.currency)
389
+ children: n == null ? t === 0 ? "Total" : "" : m(n, r?.type, r?.currency)
383
390
  }, e.key);
384
391
  })
385
392
  }) })
386
393
  ]
387
394
  }), L.length === 0 && /* @__PURE__ */ d("div", {
388
395
  className: "flex items-center justify-center py-12 text-text-secondary",
389
- children: D ? "No matching records found" : "No data available"
396
+ children: E ? "No matching records found" : "No data available"
390
397
  })]
391
398
  }),
392
- !V && y?.showPagination !== !1 && L.length > 0 && /* @__PURE__ */ d(i, {
399
+ !V && v?.showPagination !== !1 && L.length > 0 && /* @__PURE__ */ d(i, {
393
400
  pagination: {
394
- ...k,
401
+ ...O,
395
402
  totalItems: L.length
396
403
  },
397
- onPageChange: te,
398
- onPageSizeChange: ne
404
+ onPageChange: ne,
405
+ onPageSizeChange: re
399
406
  })
400
407
  ]
401
408
  });
402
409
  });
403
410
  //#endregion
404
- export { g as default };
411
+ export { h as default };
405
412
 
406
413
  //# sourceMappingURL=TableWidget.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"TableWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/table/TableWidget.tsx"],"sourcesContent":["/**\n * TableWidget Component\n *\n * Data table with sorting, filtering, and pagination.\n */\n\nimport { type FC, memo, useState, useMemo, useCallback, useRef, useEffect } from 'react';\nimport type { Widget, TableConfig, TableColumnConfig } from '../../../types';\nimport { TableHeader, type ColumnDefinition, type SortState, type SortDirection } from './TableHeader';\nimport { TableCell, type CellFormatConfig } from './TableCell';\nimport { TablePagination, type PaginationState } from './TablePagination';\nimport type { ConditionalRule } from '../utils/conditionalFormat';\n// Phase 2: Cross-filtering support\nimport { useWidgetCrossFilter, useCrossFilterContext } from '../../../contexts';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TableWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Table data (array of rows) */\n data?: Record<string, unknown>[];\n /** Click handler for row drilldown */\n onRowClick?: (row: Record<string, unknown>, index: number) => void;\n /** Drilldown handler - receives selected row data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction sortData(data: Record<string, unknown>[], sortState: SortState): Record<string, unknown>[] {\n if (!sortState.column || !sortState.direction) {\n return data;\n }\n\n return [...data].sort((a, b) => {\n const aValue = a[sortState.column!];\n const bValue = b[sortState.column!];\n\n // Handle null/undefined\n if (aValue == null && bValue == null) return 0;\n if (aValue == null) return sortState.direction === 'asc' ? 1 : -1;\n if (bValue == null) return sortState.direction === 'asc' ? -1 : 1;\n\n // Compare values\n let comparison = 0;\n if (typeof aValue === 'number' && typeof bValue === 'number') {\n comparison = aValue - bValue;\n } else {\n comparison = String(aValue).localeCompare(String(bValue));\n }\n\n return sortState.direction === 'asc' ? comparison : -comparison;\n });\n}\n\nfunction filterData(\n data: Record<string, unknown>[],\n searchQuery: string,\n columns: ColumnDefinition[]\n): Record<string, unknown>[] {\n if (!searchQuery.trim()) {\n return data;\n }\n\n const query = searchQuery.toLowerCase();\n return data.filter((row) =>\n columns.some((col) => {\n const value = row[col.key];\n return value != null && String(value).toLowerCase().includes(query);\n })\n );\n}\n\n/** Format a footer total respecting the column's cell format. */\nfunction formatTotal(value: number, formatType?: string, currency?: string): string {\n switch (formatType) {\n case 'currency':\n return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD' }).format(value);\n case 'percentage':\n return `${(value * 100).toFixed(1)}%`;\n case 'number':\n return new Intl.NumberFormat('en-US').format(value);\n default:\n return Number.isInteger(value) ? String(value) : value.toFixed(2);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TableWidget: FC<TableWidgetProps> = memo(function TableWidget({\n widget,\n data: rawData = [],\n onRowClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as TableConfig | undefined;\n\n // Normalize data - handle both array and object formats\n const data = useMemo(() => {\n // If it's already an array, use it directly\n if (Array.isArray(rawData)) {\n return rawData;\n }\n\n // If it's an object, try to find an array property\n if (rawData && typeof rawData === 'object') {\n // Check common array property names\n const possibleArrayProps = ['data', 'items', 'rows', 'results', 'records', 'list', 'sales', 'entries', 'values'];\n for (const prop of possibleArrayProps) {\n if (Array.isArray((rawData as Record<string, unknown>)[prop])) {\n return (rawData as Record<string, unknown>)[prop] as Record<string, unknown>[];\n }\n }\n\n // If object has array values, use the first one\n const values = Object.values(rawData);\n for (const value of values) {\n if (Array.isArray(value)) {\n return value as Record<string, unknown>[];\n }\n }\n\n // If it's a single object, wrap it in an array\n return [rawData as Record<string, unknown>];\n }\n\n return [];\n }, [rawData]);\n\n // Phase 2: Cross-filtering support\n const { handleDataClick: triggerCrossFilter, enabled: crossFilterEnabled } = useWidgetCrossFilter(\n widget.id,\n // Map first column to filter ID by default, can be configured\n config?.crossFilterMappings\n );\n const { clearAllCrossFilters, hasCrossFilters } = useCrossFilterContext();\n\n // State\n const [sortState, setSortState] = useState<SortState>({\n column: config?.defaultSortColumn || null,\n direction: (config?.defaultSortDirection as SortDirection) || null,\n });\n const [searchQuery, setSearchQuery] = useState('');\n const [pagination, setPagination] = useState<PaginationState>({\n page: 0,\n pageSize: config?.pageSize || 10,\n totalItems: 0,\n });\n\n // Build column definitions from config or data\n const columns: ColumnDefinition[] = useMemo(() => {\n if (config?.columns?.length) {\n return (config.columns as (string | TableColumnConfig)[]).map((col) => {\n // Handle both string and object column definitions\n if (typeof col === 'string') {\n return {\n key: col,\n header: col.charAt(0).toUpperCase() + col.slice(1).replace(/_/g, ' '),\n sortable: true,\n };\n }\n // Object column definition — accept both .key and .field\n const fieldKey = col.key || col.field || '';\n return {\n key: fieldKey,\n header: col.label || col.header || fieldKey,\n sortable: col.sortable !== false,\n width: col.width != null ? String(col.width) : undefined,\n align: col.align as ColumnDefinition['align'],\n };\n }) as ColumnDefinition[];\n }\n\n // Auto-generate from first data row\n if (data.length > 0) {\n return Object.keys(data[0]).map((key) => ({\n key,\n header: key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' '),\n sortable: true,\n }));\n }\n\n return [];\n }, [config?.columns, data]);\n\n // Build cell format configs\n const cellFormats: Record<string, CellFormatConfig> = useMemo(() => {\n const formats: Record<string, CellFormatConfig> = {};\n config?.columns?.forEach((col) => {\n if (col.format && (col.key || col.field)) {\n formats[col.key || col.field || ''] = { type: col.format };\n }\n });\n return formats;\n }, [config?.columns]);\n\n // Per-column conditional rules (column-level overrides win over table-level).\n const tableRules = config?.conditionalRules;\n const columnRules: Record<string, ConditionalRule[] | undefined> = useMemo(() => {\n const map: Record<string, ConditionalRule[] | undefined> = {};\n config?.columns?.forEach((col) => {\n const fieldKey = col.key || col.field;\n if (fieldKey && col.conditionalRules?.length) {\n map[fieldKey] = col.conditionalRules;\n }\n });\n return map;\n }, [config?.columns]);\n\n // Per-column totals/aggregates for the footer row.\n const columnTotals: Record<string, TableColumnConfig['total']> = useMemo(() => {\n const map: Record<string, TableColumnConfig['total']> = {};\n config?.columns?.forEach((col) => {\n const fieldKey = col.key || col.field;\n if (fieldKey && col.total && col.total !== 'none') {\n map[fieldKey] = col.total;\n }\n });\n return map;\n }, [config?.columns]);\n\n const hasTotals = config?.showTotals === true || Object.keys(columnTotals).length > 0;\n\n // Process data: filter, sort, paginate\n const processedData = useMemo(() => {\n let result = data;\n\n // Filter\n if (config?.showSearch !== false) {\n result = filterData(result, searchQuery, columns);\n }\n\n // Sort\n result = sortData(result, sortState);\n\n return result;\n }, [data, searchQuery, sortState, columns, config?.showSearch]);\n\n // Max absolute numeric value per column — scales in-cell data bars.\n const dataBarMaxes: Record<string, number> = useMemo(() => {\n const maxes: Record<string, number> = {};\n for (const col of columns) {\n let max = 0;\n for (const row of processedData) {\n const v = row[col.key];\n if (typeof v === 'number' && Number.isFinite(v)) {\n max = Math.max(max, Math.abs(v));\n }\n }\n maxes[col.key] = max;\n }\n return maxes;\n }, [columns, processedData]);\n\n // Footer totals/aggregates per column (computed over the filtered set).\n const footerTotals: Record<string, number | null> = useMemo(() => {\n if (!hasTotals) return {};\n const result: Record<string, number | null> = {};\n for (const col of columns) {\n const agg = columnTotals[col.key];\n if (!agg) {\n result[col.key] = null;\n continue;\n }\n const nums = processedData\n .map((row) => row[col.key])\n .filter((v): v is number => typeof v === 'number' && Number.isFinite(v));\n if (agg === 'count') {\n result[col.key] = processedData.length;\n } else if (nums.length === 0) {\n result[col.key] = null;\n } else if (agg === 'sum') {\n result[col.key] = nums.reduce((a, b) => a + b, 0);\n } else if (agg === 'avg') {\n result[col.key] = nums.reduce((a, b) => a + b, 0) / nums.length;\n } else if (agg === 'min') {\n result[col.key] = Math.min(...nums);\n } else if (agg === 'max') {\n result[col.key] = Math.max(...nums);\n } else {\n result[col.key] = null;\n }\n }\n return result;\n }, [hasTotals, columns, columnTotals, processedData]);\n\n // Update total items when data changes\n useMemo(() => {\n setPagination((prev) => ({\n ...prev,\n totalItems: processedData.length,\n }));\n }, [processedData.length]);\n\n // Paginate\n const paginatedData = useMemo(() => {\n if (config?.showPagination === false) {\n return processedData;\n }\n const start = pagination.page * pagination.pageSize;\n return processedData.slice(start, start + pagination.pageSize);\n }, [processedData, pagination, config?.showPagination]);\n\n // ----------------------------------------------------------------------------\n // Row virtualization (windowing) — render only the visible slice of a large\n // dataset. Active when config.virtualized is set; bypasses pagination so the\n // whole filtered set scrolls in one container. No external dependency: a fixed\n // row height + top/bottom spacer rows reproduce the full scroll height.\n // ----------------------------------------------------------------------------\n const virtualized = config?.virtualized === true;\n const rowHeight = config?.rowHeight && config.rowHeight > 0 ? config.rowHeight : 44;\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState(0);\n const [viewportH, setViewportH] = useState(0);\n\n useEffect(() => {\n if (!virtualized) return undefined;\n const el = scrollRef.current;\n if (!el) return undefined;\n setViewportH(el.clientHeight);\n const ro =\n typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => setViewportH(el.clientHeight)) : undefined;\n ro?.observe(el);\n return () => ro?.disconnect();\n }, [virtualized]);\n\n const OVERSCAN = 6;\n const virtualWindow = useMemo(() => {\n if (!virtualized) return null;\n const total = processedData.length;\n const visibleCount = viewportH > 0 ? Math.ceil(viewportH / rowHeight) : 20;\n const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);\n const endIndex = Math.min(total, startIndex + visibleCount + OVERSCAN * 2);\n return {\n rows: processedData.slice(startIndex, endIndex),\n topSpacer: startIndex * rowHeight,\n bottomSpacer: Math.max(0, (total - endIndex) * rowHeight),\n startIndex,\n };\n }, [virtualized, processedData, viewportH, scrollTop, rowHeight]);\n\n // The rows actually rendered in <tbody>: windowed slice or the paginated page.\n const renderRows = virtualized && virtualWindow ? virtualWindow.rows : paginatedData;\n const rowBaseIndex = virtualized && virtualWindow ? virtualWindow.startIndex : 0;\n\n // Sticky-left frozen columns: give the first N columns explicit widths so the\n // header and body align, then compute each one's cumulative left offset.\n const frozenCount = config?.frozenColumns && config.frozenColumns > 0 ? config.frozenColumns : 0;\n const FROZEN_DEFAULT_W = 160;\n const { displayColumns, frozenLefts } = useMemo(() => {\n if (frozenCount <= 0) return { displayColumns: columns, frozenLefts: {} as Record<string, number> };\n const lefts: Record<string, number> = {};\n let cursor = 0;\n const cols = columns.map((col, i) => {\n if (i < frozenCount) {\n const widthPx = col.width ? parseInt(col.width, 10) || FROZEN_DEFAULT_W : FROZEN_DEFAULT_W;\n lefts[col.key] = cursor;\n cursor += widthPx;\n return { ...col, width: `${widthPx}px` };\n }\n return col;\n });\n return { displayColumns: cols, frozenLefts: lefts };\n }, [columns, frozenCount]);\n\n // Handlers\n const handleSort = useCallback((column: string) => {\n setSortState((prev) => {\n if (prev.column !== column) {\n return { column, direction: 'asc' };\n }\n const nextDirection: SortDirection = prev.direction === 'asc' ? 'desc' : prev.direction === 'desc' ? null : 'asc';\n return {\n column: nextDirection ? column : null,\n direction: nextDirection,\n };\n });\n }, []);\n\n const handlePageChange = useCallback((page: number) => {\n setPagination((prev) => ({ ...prev, page }));\n }, []);\n\n const handlePageSizeChange = useCallback((pageSize: number) => {\n setPagination((prev) => ({ ...prev, pageSize, page: 0 }));\n }, []);\n\n const handleRowClick = useCallback(\n (row: Record<string, unknown>, index: number) => {\n // Trigger cross-filtering on row click when explicitly enabled via config\n const hasCrossFilterConfig = config?.crossFilterMappings?.length || config?.enableCrossFilter;\n if (crossFilterEnabled && hasCrossFilterConfig) {\n const firstColumn = columns[0]?.key;\n const label = firstColumn ? String(row[firstColumn]) : undefined;\n triggerCrossFilter(row as Record<string, unknown>, label);\n }\n\n // Trigger drilldown if configured\n if (onDrilldown) {\n onDrilldown(row);\n } else if (onRowClick && widget.drilldowns?.length) {\n onRowClick(row, index);\n }\n },\n [\n onRowClick,\n onDrilldown,\n widget.drilldowns,\n crossFilterEnabled,\n triggerCrossFilter,\n columns,\n config?.crossFilterMappings,\n config?.enableCrossFilter,\n ]\n );\n\n // Export to CSV\n const handleExport = useCallback(() => {\n const headers = columns.map((c) => c.header).join(',');\n const rows = processedData.map((row) =>\n columns\n .map((col) => {\n const value = row[col.key];\n if (value == null) return '';\n const str = String(value);\n // Escape quotes and wrap in quotes if contains comma\n if (str.includes(',') || str.includes('\"')) {\n return `\"${str.replace(/\"/g, '\"\"')}\"`;\n }\n return str;\n })\n .join(',')\n );\n const csv = [headers, ...rows].join('\\n');\n const blob = new Blob([csv], { type: 'text/csv' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `${widget.title || 'table'}-${new Date().toISOString().split('T')[0]}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n }, [columns, processedData, widget.title]);\n\n // Row is only clickable if drilldown or cross-filter is explicitly configured\n const hasCrossFilterConfig = config?.crossFilterMappings?.length || config?.enableCrossFilter;\n const isClickable = Boolean(onDrilldown || (onRowClick && widget.drilldowns?.length) || hasCrossFilterConfig);\n\n return (\n <div className=\"flex flex-col h-full\">\n {/* Cross-filter active indicator with clear button */}\n {hasCrossFilters && (\n <div className=\"flex items-center gap-2 px-3 py-2 border-b border-border-default\">\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n clearAllCrossFilters();\n }}\n className=\"\n inline-flex items-center gap-1.5\n px-2.5 py-1 rounded-md\n text-xs font-medium\n bg-action-primary-bg/10 text-action-primary-text\n hover:bg-action-primary-bg/20\n transition-colors\n \"\n >\n <svg className=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 19l-7-7m0 0l7-7m-7 7h18\" />\n </svg>\n Clear Filter\n </button>\n </div>\n )}\n\n {/* Toolbar */}\n {(config?.showSearch !== false || config?.showExport !== false) && (\n <div className=\"flex items-center justify-between p-3 border-b border-border-default\">\n {/* Search */}\n {config?.showSearch !== false && (\n <div className=\"relative flex items-center\">\n <svg\n className=\"absolute left-3 w-4 h-4 text-text-secondary pointer-events-none\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n />\n </svg>\n <input\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"\n pl-10 pr-4 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg focus:border-action-primary-bg\n w-48 sm:w-64\n placeholder:text-text-tertiary\n \"\n />\n </div>\n )}\n\n {/* Export */}\n {config?.showExport !== false && (\n <button\n type=\"button\"\n onClick={handleExport}\n className=\"\n inline-flex items-center gap-2\n px-3 py-2\n text-sm font-medium\n text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n hover:bg-bg-muted\n transition-colors\n \"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\"\n />\n </svg>\n Export CSV\n </button>\n )}\n </div>\n )}\n\n {/* Table */}\n <div\n ref={scrollRef}\n className=\"flex-1 overflow-auto\"\n onScroll={virtualized ? (e) => setScrollTop((e.target as HTMLDivElement).scrollTop) : undefined}\n >\n <table className=\"w-full\">\n <TableHeader\n columns={displayColumns}\n sortState={sortState}\n onSort={handleSort}\n frozenLefts={frozenCount > 0 ? frozenLefts : undefined}\n />\n <tbody>\n {/* Virtualization top spacer */}\n {virtualized && virtualWindow && virtualWindow.topSpacer > 0 && (\n <tr aria-hidden=\"true\" style={{ height: virtualWindow.topSpacer }}>\n <td colSpan={displayColumns.length} style={{ padding: 0, border: 'none' }} />\n </tr>\n )}\n {renderRows.map((row, i) => {\n const rowIndex = rowBaseIndex + i;\n return (\n <tr\n key={rowIndex}\n className={`\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50' : ''}\n ${rowIndex % 2 === 0 ? 'bg-bg-surface' : 'bg-bg-muted/30'}\n `}\n style={virtualized ? { height: rowHeight } : undefined}\n onClick={() => handleRowClick(row, rowIndex)}\n >\n {displayColumns.map((column) => (\n <TableCell\n key={column.key}\n value={row[column.key]}\n column={column}\n format={cellFormats[column.key]}\n rowData={row}\n conditionalRules={columnRules[column.key] ?? tableRules}\n dataBarMax={dataBarMaxes[column.key]}\n stickyLeft={frozenLefts[column.key]}\n />\n ))}\n </tr>\n );\n })}\n {/* Virtualization bottom spacer */}\n {virtualized && virtualWindow && virtualWindow.bottomSpacer > 0 && (\n <tr aria-hidden=\"true\" style={{ height: virtualWindow.bottomSpacer }}>\n <td colSpan={displayColumns.length} style={{ padding: 0, border: 'none' }} />\n </tr>\n )}\n </tbody>\n {hasTotals && processedData.length > 0 && (\n <tfoot>\n <tr className=\"bg-bg-muted/50 font-semibold border-t-2 border-border-default\">\n {displayColumns.map((column, colIndex) => {\n const total = footerTotals[column.key];\n const format = cellFormats[column.key];\n const frozen = frozenLefts[column.key];\n return (\n <td\n key={column.key}\n className={`\n px-4 py-3 text-sm text-text-primary\n ${column.align === 'center' ? 'text-center' : ''}\n ${column.align === 'right' ? 'text-right' : 'text-left'}\n ${frozen != null ? 'sticky z-10 bg-bg-muted' : ''}\n `}\n style={frozen != null ? { left: frozen } : undefined}\n >\n {total != null\n ? formatTotal(total, format?.type, format?.currency)\n : colIndex === 0\n ? 'Total'\n : ''}\n </td>\n );\n })}\n </tr>\n </tfoot>\n )}\n </table>\n\n {processedData.length === 0 && (\n <div className=\"flex items-center justify-center py-12 text-text-secondary\">\n {searchQuery ? 'No matching records found' : 'No data available'}\n </div>\n )}\n </div>\n\n {/* Pagination (suppressed in virtualized mode — the whole set scrolls) */}\n {!virtualized && config?.showPagination !== false && processedData.length > 0 && (\n <TablePagination\n pagination={{ ...pagination, totalItems: processedData.length }}\n onPageChange={handlePageChange}\n onPageSizeChange={handlePageSizeChange}\n />\n )}\n </div>\n );\n});\n\nexport default TableWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"TableWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/table/TableWidget.tsx"],"sourcesContent":["/**\n * TableWidget Component\n *\n * Data table with sorting, filtering, and pagination.\n */\n\nimport { type FC, memo, useState, useMemo, useCallback, useRef, useEffect } from 'react';\nimport type { Widget, TableConfig, TableColumnConfig } from '../../../types';\nimport { TableHeader, type ColumnDefinition, type SortState, type SortDirection } from './TableHeader';\nimport { TableCell, type CellFormatConfig, type CellChart } from './TableCell';\nimport { TablePagination, type PaginationState } from './TablePagination';\nimport type { ConditionalRule } from '../utils/conditionalFormat';\n// Phase 2: Cross-filtering support\nimport { useWidgetCrossFilter, useCrossFilterContext } from '../../../contexts';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TableWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Table data (array of rows) */\n data?: Record<string, unknown>[];\n /** Click handler for row drilldown */\n onRowClick?: (row: Record<string, unknown>, index: number) => void;\n /** Drilldown handler - receives selected row data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction sortData(data: Record<string, unknown>[], sortState: SortState): Record<string, unknown>[] {\n if (!sortState.column || !sortState.direction) {\n return data;\n }\n\n return [...data].sort((a, b) => {\n const aValue = a[sortState.column!];\n const bValue = b[sortState.column!];\n\n // Handle null/undefined\n if (aValue == null && bValue == null) return 0;\n if (aValue == null) return sortState.direction === 'asc' ? 1 : -1;\n if (bValue == null) return sortState.direction === 'asc' ? -1 : 1;\n\n // Compare values\n let comparison = 0;\n if (typeof aValue === 'number' && typeof bValue === 'number') {\n comparison = aValue - bValue;\n } else {\n comparison = String(aValue).localeCompare(String(bValue));\n }\n\n return sortState.direction === 'asc' ? comparison : -comparison;\n });\n}\n\nfunction filterData(\n data: Record<string, unknown>[],\n searchQuery: string,\n columns: ColumnDefinition[]\n): Record<string, unknown>[] {\n if (!searchQuery.trim()) {\n return data;\n }\n\n const query = searchQuery.toLowerCase();\n return data.filter((row) =>\n columns.some((col) => {\n const value = row[col.key];\n return value != null && String(value).toLowerCase().includes(query);\n })\n );\n}\n\n/** Format a footer total respecting the column's cell format. */\nfunction formatTotal(value: number, formatType?: string, currency?: string): string {\n switch (formatType) {\n case 'currency':\n return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD' }).format(value);\n case 'percentage':\n return `${(value * 100).toFixed(1)}%`;\n case 'number':\n return new Intl.NumberFormat('en-US').format(value);\n default:\n return Number.isInteger(value) ? String(value) : value.toFixed(2);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const TableWidget: FC<TableWidgetProps> = memo(function TableWidget({\n widget,\n data: rawData = [],\n onRowClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as TableConfig | undefined;\n\n // Normalize data - handle both array and object formats\n const data = useMemo(() => {\n // If it's already an array, use it directly\n if (Array.isArray(rawData)) {\n return rawData;\n }\n\n // If it's an object, try to find an array property\n if (rawData && typeof rawData === 'object') {\n // Check common array property names\n const possibleArrayProps = ['data', 'items', 'rows', 'results', 'records', 'list', 'sales', 'entries', 'values'];\n for (const prop of possibleArrayProps) {\n if (Array.isArray((rawData as Record<string, unknown>)[prop])) {\n return (rawData as Record<string, unknown>)[prop] as Record<string, unknown>[];\n }\n }\n\n // If object has array values, use the first one\n const values = Object.values(rawData);\n for (const value of values) {\n if (Array.isArray(value)) {\n return value as Record<string, unknown>[];\n }\n }\n\n // If it's a single object, wrap it in an array\n return [rawData as Record<string, unknown>];\n }\n\n return [];\n }, [rawData]);\n\n // Phase 2: Cross-filtering support\n const { handleDataClick: triggerCrossFilter, enabled: crossFilterEnabled } = useWidgetCrossFilter(\n widget.id,\n // Map first column to filter ID by default, can be configured\n config?.crossFilterMappings\n );\n const { clearAllCrossFilters, hasCrossFilters } = useCrossFilterContext();\n\n // State\n const [sortState, setSortState] = useState<SortState>({\n column: config?.defaultSortColumn || null,\n direction: (config?.defaultSortDirection as SortDirection) || null,\n });\n const [searchQuery, setSearchQuery] = useState('');\n const [pagination, setPagination] = useState<PaginationState>({\n page: 0,\n pageSize: config?.pageSize || 10,\n totalItems: 0,\n });\n\n // Build column definitions from config or data\n const columns: ColumnDefinition[] = useMemo(() => {\n if (config?.columns?.length) {\n return (config.columns as (string | TableColumnConfig)[]).map((col) => {\n // Handle both string and object column definitions\n if (typeof col === 'string') {\n return {\n key: col,\n header: col.charAt(0).toUpperCase() + col.slice(1).replace(/_/g, ' '),\n sortable: true,\n };\n }\n // Object column definition — accept both .key and .field\n const fieldKey = col.key || col.field || '';\n return {\n key: fieldKey,\n header: col.label || col.header || fieldKey,\n sortable: col.sortable !== false,\n width: col.width != null ? String(col.width) : undefined,\n align: col.align as ColumnDefinition['align'],\n };\n }) as ColumnDefinition[];\n }\n\n // Auto-generate from first data row\n if (data.length > 0) {\n return Object.keys(data[0]).map((key) => ({\n key,\n header: key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' '),\n sortable: true,\n }));\n }\n\n return [];\n }, [config?.columns, data]);\n\n // Build cell format configs\n const cellFormats: Record<string, CellFormatConfig> = useMemo(() => {\n const formats: Record<string, CellFormatConfig> = {};\n config?.columns?.forEach((col) => {\n if (col.format && (col.key || col.field)) {\n formats[col.key || col.field || ''] = { type: col.format };\n }\n });\n return formats;\n }, [config?.columns]);\n\n // Per-column in-cell chart kind (e.g. sparkline).\n const columnCellCharts: Record<string, CellChart | undefined> = useMemo(() => {\n const map: Record<string, CellChart | undefined> = {};\n config?.columns?.forEach((col) => {\n const fieldKey = col.key || col.field;\n if (fieldKey && col.cellChart) {\n map[fieldKey] = col.cellChart;\n }\n });\n return map;\n }, [config?.columns]);\n\n // Per-column conditional rules (column-level overrides win over table-level).\n const tableRules = config?.conditionalRules;\n const columnRules: Record<string, ConditionalRule[] | undefined> = useMemo(() => {\n const map: Record<string, ConditionalRule[] | undefined> = {};\n config?.columns?.forEach((col) => {\n const fieldKey = col.key || col.field;\n if (fieldKey && col.conditionalRules?.length) {\n map[fieldKey] = col.conditionalRules;\n }\n });\n return map;\n }, [config?.columns]);\n\n // Per-column totals/aggregates for the footer row.\n const columnTotals: Record<string, TableColumnConfig['total']> = useMemo(() => {\n const map: Record<string, TableColumnConfig['total']> = {};\n config?.columns?.forEach((col) => {\n const fieldKey = col.key || col.field;\n if (fieldKey && col.total && col.total !== 'none') {\n map[fieldKey] = col.total;\n }\n });\n return map;\n }, [config?.columns]);\n\n const hasTotals = config?.showTotals === true || Object.keys(columnTotals).length > 0;\n\n // Process data: filter, sort, paginate\n const processedData = useMemo(() => {\n let result = data;\n\n // Filter\n if (config?.showSearch !== false) {\n result = filterData(result, searchQuery, columns);\n }\n\n // Sort\n result = sortData(result, sortState);\n\n return result;\n }, [data, searchQuery, sortState, columns, config?.showSearch]);\n\n // Max absolute numeric value per column — scales in-cell data bars.\n const dataBarMaxes: Record<string, number> = useMemo(() => {\n const maxes: Record<string, number> = {};\n for (const col of columns) {\n let max = 0;\n for (const row of processedData) {\n const v = row[col.key];\n if (typeof v === 'number' && Number.isFinite(v)) {\n max = Math.max(max, Math.abs(v));\n }\n }\n maxes[col.key] = max;\n }\n return maxes;\n }, [columns, processedData]);\n\n // Footer totals/aggregates per column (computed over the filtered set).\n const footerTotals: Record<string, number | null> = useMemo(() => {\n if (!hasTotals) return {};\n const result: Record<string, number | null> = {};\n for (const col of columns) {\n const agg = columnTotals[col.key];\n if (!agg) {\n result[col.key] = null;\n continue;\n }\n const nums = processedData\n .map((row) => row[col.key])\n .filter((v): v is number => typeof v === 'number' && Number.isFinite(v));\n if (agg === 'count') {\n result[col.key] = processedData.length;\n } else if (nums.length === 0) {\n result[col.key] = null;\n } else if (agg === 'sum') {\n result[col.key] = nums.reduce((a, b) => a + b, 0);\n } else if (agg === 'avg') {\n result[col.key] = nums.reduce((a, b) => a + b, 0) / nums.length;\n } else if (agg === 'min') {\n result[col.key] = Math.min(...nums);\n } else if (agg === 'max') {\n result[col.key] = Math.max(...nums);\n } else {\n result[col.key] = null;\n }\n }\n return result;\n }, [hasTotals, columns, columnTotals, processedData]);\n\n // Update total items when data changes\n useMemo(() => {\n setPagination((prev) => ({\n ...prev,\n totalItems: processedData.length,\n }));\n }, [processedData.length]);\n\n // Paginate\n const paginatedData = useMemo(() => {\n if (config?.showPagination === false) {\n return processedData;\n }\n const start = pagination.page * pagination.pageSize;\n return processedData.slice(start, start + pagination.pageSize);\n }, [processedData, pagination, config?.showPagination]);\n\n // ----------------------------------------------------------------------------\n // Row virtualization (windowing) — render only the visible slice of a large\n // dataset. Active when config.virtualized is set; bypasses pagination so the\n // whole filtered set scrolls in one container. No external dependency: a fixed\n // row height + top/bottom spacer rows reproduce the full scroll height.\n // ----------------------------------------------------------------------------\n const virtualized = config?.virtualized === true;\n const rowHeight = config?.rowHeight && config.rowHeight > 0 ? config.rowHeight : 44;\n const scrollRef = useRef<HTMLDivElement>(null);\n const [scrollTop, setScrollTop] = useState(0);\n const [viewportH, setViewportH] = useState(0);\n\n useEffect(() => {\n if (!virtualized) return undefined;\n const el = scrollRef.current;\n if (!el) return undefined;\n setViewportH(el.clientHeight);\n const ro =\n typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => setViewportH(el.clientHeight)) : undefined;\n ro?.observe(el);\n return () => ro?.disconnect();\n }, [virtualized]);\n\n const OVERSCAN = 6;\n const virtualWindow = useMemo(() => {\n if (!virtualized) return null;\n const total = processedData.length;\n const visibleCount = viewportH > 0 ? Math.ceil(viewportH / rowHeight) : 20;\n const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);\n const endIndex = Math.min(total, startIndex + visibleCount + OVERSCAN * 2);\n return {\n rows: processedData.slice(startIndex, endIndex),\n topSpacer: startIndex * rowHeight,\n bottomSpacer: Math.max(0, (total - endIndex) * rowHeight),\n startIndex,\n };\n }, [virtualized, processedData, viewportH, scrollTop, rowHeight]);\n\n // The rows actually rendered in <tbody>: windowed slice or the paginated page.\n const renderRows = virtualized && virtualWindow ? virtualWindow.rows : paginatedData;\n const rowBaseIndex = virtualized && virtualWindow ? virtualWindow.startIndex : 0;\n\n // Sticky-left frozen columns: give the first N columns explicit widths so the\n // header and body align, then compute each one's cumulative left offset.\n const frozenCount = config?.frozenColumns && config.frozenColumns > 0 ? config.frozenColumns : 0;\n const FROZEN_DEFAULT_W = 160;\n const { displayColumns, frozenLefts } = useMemo(() => {\n if (frozenCount <= 0) return { displayColumns: columns, frozenLefts: {} as Record<string, number> };\n const lefts: Record<string, number> = {};\n let cursor = 0;\n const cols = columns.map((col, i) => {\n if (i < frozenCount) {\n const widthPx = col.width ? parseInt(col.width, 10) || FROZEN_DEFAULT_W : FROZEN_DEFAULT_W;\n lefts[col.key] = cursor;\n cursor += widthPx;\n return { ...col, width: `${widthPx}px` };\n }\n return col;\n });\n return { displayColumns: cols, frozenLefts: lefts };\n }, [columns, frozenCount]);\n\n // Handlers\n const handleSort = useCallback((column: string) => {\n setSortState((prev) => {\n if (prev.column !== column) {\n return { column, direction: 'asc' };\n }\n const nextDirection: SortDirection = prev.direction === 'asc' ? 'desc' : prev.direction === 'desc' ? null : 'asc';\n return {\n column: nextDirection ? column : null,\n direction: nextDirection,\n };\n });\n }, []);\n\n const handlePageChange = useCallback((page: number) => {\n setPagination((prev) => ({ ...prev, page }));\n }, []);\n\n const handlePageSizeChange = useCallback((pageSize: number) => {\n setPagination((prev) => ({ ...prev, pageSize, page: 0 }));\n }, []);\n\n const handleRowClick = useCallback(\n (row: Record<string, unknown>, index: number) => {\n // Trigger cross-filtering on row click when explicitly enabled via config\n const hasCrossFilterConfig = config?.crossFilterMappings?.length || config?.enableCrossFilter;\n if (crossFilterEnabled && hasCrossFilterConfig) {\n const firstColumn = columns[0]?.key;\n const label = firstColumn ? String(row[firstColumn]) : undefined;\n triggerCrossFilter(row as Record<string, unknown>, label);\n }\n\n // Trigger drilldown if configured\n if (onDrilldown) {\n onDrilldown(row);\n } else if (onRowClick && widget.drilldowns?.length) {\n onRowClick(row, index);\n }\n },\n [\n onRowClick,\n onDrilldown,\n widget.drilldowns,\n crossFilterEnabled,\n triggerCrossFilter,\n columns,\n config?.crossFilterMappings,\n config?.enableCrossFilter,\n ]\n );\n\n // Export to CSV\n const handleExport = useCallback(() => {\n const headers = columns.map((c) => c.header).join(',');\n const rows = processedData.map((row) =>\n columns\n .map((col) => {\n const value = row[col.key];\n if (value == null) return '';\n const str = String(value);\n // Escape quotes and wrap in quotes if contains comma\n if (str.includes(',') || str.includes('\"')) {\n return `\"${str.replace(/\"/g, '\"\"')}\"`;\n }\n return str;\n })\n .join(',')\n );\n const csv = [headers, ...rows].join('\\n');\n const blob = new Blob([csv], { type: 'text/csv' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `${widget.title || 'table'}-${new Date().toISOString().split('T')[0]}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n }, [columns, processedData, widget.title]);\n\n // Row is only clickable if drilldown or cross-filter is explicitly configured\n const hasCrossFilterConfig = config?.crossFilterMappings?.length || config?.enableCrossFilter;\n const isClickable = Boolean(onDrilldown || (onRowClick && widget.drilldowns?.length) || hasCrossFilterConfig);\n\n return (\n <div className=\"flex flex-col h-full\">\n {/* Cross-filter active indicator with clear button */}\n {hasCrossFilters && (\n <div className=\"flex items-center gap-2 px-3 py-2 border-b border-border-default\">\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n clearAllCrossFilters();\n }}\n className=\"\n inline-flex items-center gap-1.5\n px-2.5 py-1 rounded-md\n text-xs font-medium\n bg-action-primary-bg/10 text-action-primary-text\n hover:bg-action-primary-bg/20\n transition-colors\n \"\n >\n <svg className=\"w-3.5 h-3.5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M10 19l-7-7m0 0l7-7m-7 7h18\" />\n </svg>\n Clear Filter\n </button>\n </div>\n )}\n\n {/* Toolbar */}\n {(config?.showSearch !== false || config?.showExport !== false) && (\n <div className=\"flex items-center justify-between p-3 border-b border-border-default\">\n {/* Search */}\n {config?.showSearch !== false && (\n <div className=\"relative flex items-center\">\n <svg\n className=\"absolute left-3 w-4 h-4 text-text-secondary pointer-events-none\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"\n />\n </svg>\n <input\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"\n pl-10 pr-4 py-2\n text-sm text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n focus:outline-none focus:ring-2 focus:ring-action-primary-bg focus:border-action-primary-bg\n w-48 sm:w-64\n placeholder:text-text-tertiary\n \"\n />\n </div>\n )}\n\n {/* Export */}\n {config?.showExport !== false && (\n <button\n type=\"button\"\n onClick={handleExport}\n className=\"\n inline-flex items-center gap-2\n px-3 py-2\n text-sm font-medium\n text-text-primary\n bg-bg-surface\n border border-border-default\n rounded-md\n hover:bg-bg-muted\n transition-colors\n \"\n >\n <svg className=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\"\n />\n </svg>\n Export CSV\n </button>\n )}\n </div>\n )}\n\n {/* Table */}\n <div\n ref={scrollRef}\n className=\"flex-1 overflow-auto\"\n onScroll={virtualized ? (e) => setScrollTop((e.target as HTMLDivElement).scrollTop) : undefined}\n >\n <table className=\"w-full\">\n <TableHeader\n columns={displayColumns}\n sortState={sortState}\n onSort={handleSort}\n frozenLefts={frozenCount > 0 ? frozenLefts : undefined}\n />\n <tbody>\n {/* Virtualization top spacer */}\n {virtualized && virtualWindow && virtualWindow.topSpacer > 0 && (\n <tr aria-hidden=\"true\" style={{ height: virtualWindow.topSpacer }}>\n <td colSpan={displayColumns.length} style={{ padding: 0, border: 'none' }} />\n </tr>\n )}\n {renderRows.map((row, i) => {\n const rowIndex = rowBaseIndex + i;\n return (\n <tr\n key={rowIndex}\n className={`\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50' : ''}\n ${rowIndex % 2 === 0 ? 'bg-bg-surface' : 'bg-bg-muted/30'}\n `}\n style={virtualized ? { height: rowHeight } : undefined}\n onClick={() => handleRowClick(row, rowIndex)}\n >\n {displayColumns.map((column) => (\n <TableCell\n key={column.key}\n value={row[column.key]}\n column={column}\n format={cellFormats[column.key]}\n rowData={row}\n conditionalRules={columnRules[column.key] ?? tableRules}\n dataBarMax={dataBarMaxes[column.key]}\n stickyLeft={frozenLefts[column.key]}\n cellChart={columnCellCharts[column.key]}\n />\n ))}\n </tr>\n );\n })}\n {/* Virtualization bottom spacer */}\n {virtualized && virtualWindow && virtualWindow.bottomSpacer > 0 && (\n <tr aria-hidden=\"true\" style={{ height: virtualWindow.bottomSpacer }}>\n <td colSpan={displayColumns.length} style={{ padding: 0, border: 'none' }} />\n </tr>\n )}\n </tbody>\n {hasTotals && processedData.length > 0 && (\n <tfoot>\n <tr className=\"bg-bg-muted/50 font-semibold border-t-2 border-border-default\">\n {displayColumns.map((column, colIndex) => {\n const total = footerTotals[column.key];\n const format = cellFormats[column.key];\n const frozen = frozenLefts[column.key];\n return (\n <td\n key={column.key}\n className={`\n px-4 py-3 text-sm text-text-primary\n ${column.align === 'center' ? 'text-center' : ''}\n ${column.align === 'right' ? 'text-right' : 'text-left'}\n ${frozen != null ? 'sticky z-10 bg-bg-muted' : ''}\n `}\n style={frozen != null ? { left: frozen } : undefined}\n >\n {total != null\n ? formatTotal(total, format?.type, format?.currency)\n : colIndex === 0\n ? 'Total'\n : ''}\n </td>\n );\n })}\n </tr>\n </tfoot>\n )}\n </table>\n\n {processedData.length === 0 && (\n <div className=\"flex items-center justify-center py-12 text-text-secondary\">\n {searchQuery ? 'No matching records found' : 'No data available'}\n </div>\n )}\n </div>\n\n {/* Pagination (suppressed in virtualized mode — the whole set scrolls) */}\n {!virtualized && config?.showPagination !== false && processedData.length > 0 && (\n <TablePagination\n pagination={{ ...pagination, totalItems: processedData.length }}\n onPageChange={handlePageChange}\n onPageSizeChange={handlePageSizeChange}\n />\n )}\n </div>\n );\n});\n\nexport default TableWidget;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-bigconsole",
3
- "version": "2026.613.4",
3
+ "version": "2026.613.5",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [