@burdenoff/microfe-bigconsole 2026.624.1 → 2026.624.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/bigconsole/BigConsoleRoutes.js +1 -1
  2. package/dist/bigconsole/BigConsoleRoutes.js.map +1 -1
  3. package/dist/bigconsole/components/DrilldownModal.js +4 -4
  4. package/dist/bigconsole/components/DrilldownModal.js.map +1 -1
  5. package/dist/bigconsole/components/dashboard/DashboardCanvas.js +1 -1
  6. package/dist/bigconsole/components/dashboard/DashboardCanvas.js.map +1 -1
  7. package/dist/bigconsole/components/dashboard/DashboardToolbar.js +2 -2
  8. package/dist/bigconsole/components/dashboard/DashboardToolbar.js.map +1 -1
  9. package/dist/bigconsole/components/dashboard/DrilldownBreadcrumb.js +1 -1
  10. package/dist/bigconsole/components/dashboard/DrilldownBreadcrumb.js.map +1 -1
  11. package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js +28 -28
  12. package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js.map +1 -1
  13. package/dist/bigconsole/components/datasink/CacheTTLSelector.js +1 -1
  14. package/dist/bigconsole/components/datasink/CacheTTLSelector.js.map +1 -1
  15. package/dist/bigconsole/components/datasink/DataSinkCard.js +4 -4
  16. package/dist/bigconsole/components/datasink/DataSinkCard.js.map +1 -1
  17. package/dist/bigconsole/components/datasink/DataSinkDataViewer.js +2 -2
  18. package/dist/bigconsole/components/datasink/DataSinkDataViewer.js.map +1 -1
  19. package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js +2 -2
  20. package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js.map +1 -1
  21. package/dist/bigconsole/components/datasink/DataSinkRefreshDialog.js +2 -2
  22. package/dist/bigconsole/components/datasink/DataSinkRefreshDialog.js.map +1 -1
  23. package/dist/bigconsole/components/datasink/DataSinkTableViewer.js +2 -2
  24. package/dist/bigconsole/components/datasink/DataSinkTableViewer.js.map +1 -1
  25. package/dist/bigconsole/components/datasink/ImportDataSinkDialog.js +25 -25
  26. package/dist/bigconsole/components/datasink/ImportDataSinkDialog.js.map +1 -1
  27. package/dist/bigconsole/components/datasink/LinkedParsersPanel.js +1 -1
  28. package/dist/bigconsole/components/datasink/LinkedParsersPanel.js.map +1 -1
  29. package/dist/bigconsole/components/datasink/LinkedWorkflowsPanel.js +3 -3
  30. package/dist/bigconsole/components/datasink/LinkedWorkflowsPanel.js.map +1 -1
  31. package/dist/bigconsole/components/datasink/RefreshTypeSelector.js +2 -2
  32. package/dist/bigconsole/components/datasink/RefreshTypeSelector.js.map +1 -1
  33. package/dist/bigconsole/components/datasink/WorkflowStatusPanel.js +2 -2
  34. package/dist/bigconsole/components/datasink/WorkflowStatusPanel.js.map +1 -1
  35. package/dist/bigconsole/components/parser/DataSinkSelector.js +3 -3
  36. package/dist/bigconsole/components/parser/DataSinkSelector.js.map +1 -1
  37. package/dist/bigconsole/components/parser/ImportParserDialog.js +26 -26
  38. package/dist/bigconsole/components/parser/ImportParserDialog.js.map +1 -1
  39. package/dist/bigconsole/components/parser/ParserCard.js +5 -5
  40. package/dist/bigconsole/components/parser/ParserCard.js.map +1 -1
  41. package/dist/bigconsole/components/parser/ParserSelector.js +6 -6
  42. package/dist/bigconsole/components/parser/ParserSelector.js.map +1 -1
  43. package/dist/bigconsole/components/parser/ParserStatusBadge.js +1 -1
  44. package/dist/bigconsole/components/parser/ParserStatusBadge.js.map +1 -1
  45. package/dist/bigconsole/components/parser/ParserTypeBadge.js +1 -1
  46. package/dist/bigconsole/components/parser/ParserTypeBadge.js.map +1 -1
  47. package/dist/bigconsole/components/pipeline/PipelineCard.js +2 -2
  48. package/dist/bigconsole/components/pipeline/PipelineCard.js.map +1 -1
  49. package/dist/bigconsole/components/pipeline/PipelineDeleteDialog.js +1 -1
  50. package/dist/bigconsole/components/pipeline/PipelineDeleteDialog.js.map +1 -1
  51. package/dist/bigconsole/components/pipeline/PipelineStatusBadge.js +1 -1
  52. package/dist/bigconsole/components/pipeline/PipelineStatusBadge.js.map +1 -1
  53. package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js +11 -11
  54. package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js.map +1 -1
  55. package/dist/bigconsole/components/widgets/PropertiesPanel.js +17 -17
  56. package/dist/bigconsole/components/widgets/PropertiesPanel.js.map +1 -1
  57. package/dist/bigconsole/components/widgets/WidgetConfigDialog.js +14 -14
  58. package/dist/bigconsole/components/widgets/WidgetConfigDialog.js.map +1 -1
  59. package/dist/bigconsole/components/widgets/WidgetHeader.js +4 -4
  60. package/dist/bigconsole/components/widgets/WidgetHeader.js.map +1 -1
  61. package/dist/bigconsole/components/widgets/WidgetMenu.js +3 -3
  62. package/dist/bigconsole/components/widgets/WidgetMenu.js.map +1 -1
  63. package/dist/bigconsole/components/widgets/WidgetWrapper.js +1 -1
  64. package/dist/bigconsole/components/widgets/WidgetWrapper.js.map +1 -1
  65. package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js +1 -1
  66. package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js.map +1 -1
  67. package/dist/bigconsole/components/widgets/comments/WidgetComments.js +2 -2
  68. package/dist/bigconsole/components/widgets/comments/WidgetComments.js.map +1 -1
  69. package/dist/bigconsole/components/widgets/custom/CustomWidget.js +5 -5
  70. package/dist/bigconsole/components/widgets/custom/CustomWidget.js.map +1 -1
  71. package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js +1 -1
  72. package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js.map +1 -1
  73. package/dist/bigconsole/components/widgets/gauge/LinearGauge.js +1 -1
  74. package/dist/bigconsole/components/widgets/gauge/LinearGauge.js.map +1 -1
  75. package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +2 -2
  76. package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js.map +1 -1
  77. package/dist/bigconsole/components/widgets/kpi/KPIWidget.js +2 -2
  78. package/dist/bigconsole/components/widgets/kpi/KPIWidget.js.map +1 -1
  79. package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js +2 -2
  80. package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js.map +1 -1
  81. package/dist/bigconsole/components/widgets/list/ListItem.js +3 -3
  82. package/dist/bigconsole/components/widgets/list/ListItem.js.map +1 -1
  83. package/dist/bigconsole/components/widgets/map-widget/MapWidget.js +3 -3
  84. package/dist/bigconsole/components/widgets/map-widget/MapWidget.js.map +1 -1
  85. package/dist/bigconsole/components/widgets/metric-card/ComparisonBadge.js +1 -1
  86. package/dist/bigconsole/components/widgets/metric-card/ComparisonBadge.js.map +1 -1
  87. package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +2 -2
  88. package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js.map +1 -1
  89. package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js +12 -12
  90. package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js.map +1 -1
  91. package/dist/bigconsole/components/widgets/progress/ProgressBar.js +2 -2
  92. package/dist/bigconsole/components/widgets/progress/ProgressBar.js.map +1 -1
  93. package/dist/bigconsole/components/widgets/progress/ProgressConfig.js +1 -1
  94. package/dist/bigconsole/components/widgets/progress/ProgressConfig.js.map +1 -1
  95. package/dist/bigconsole/components/widgets/progress/ProgressWidget.js +1 -1
  96. package/dist/bigconsole/components/widgets/progress/ProgressWidget.js.map +1 -1
  97. package/dist/bigconsole/components/widgets/retention/RetentionWidget.js +2 -2
  98. package/dist/bigconsole/components/widgets/retention/RetentionWidget.js.map +1 -1
  99. package/dist/bigconsole/components/widgets/states/EmptyState.js +1 -1
  100. package/dist/bigconsole/components/widgets/states/EmptyState.js.map +1 -1
  101. package/dist/bigconsole/components/widgets/table/TableCell.js +1 -1
  102. package/dist/bigconsole/components/widgets/table/TableCell.js.map +1 -1
  103. package/dist/bigconsole/components/widgets/table/TableConfig.js +1 -1
  104. package/dist/bigconsole/components/widgets/table/TableConfig.js.map +1 -1
  105. package/dist/bigconsole/components/widgets/table/TableHeader.js +2 -2
  106. package/dist/bigconsole/components/widgets/table/TableHeader.js.map +1 -1
  107. package/dist/bigconsole/components/widgets/table/TablePagination.js +4 -4
  108. package/dist/bigconsole/components/widgets/table/TablePagination.js.map +1 -1
  109. package/dist/bigconsole/components/widgets/table/TableWidget.js +5 -5
  110. package/dist/bigconsole/components/widgets/table/TableWidget.js.map +1 -1
  111. package/dist/bigconsole/components/widgets/text-widget/TextWidget.js +1 -1
  112. package/dist/bigconsole/components/widgets/text-widget/TextWidget.js.map +1 -1
  113. package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js +3 -3
  114. package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js.map +1 -1
  115. package/dist/bigconsole/components/widgets/utils/conditionalFormat.js +2 -2
  116. package/dist/bigconsole/components/widgets/utils/conditionalFormat.js.map +1 -1
  117. package/dist/bigconsole/components/widgets/widget-actions/WidgetActions.js +3 -3
  118. package/dist/bigconsole/components/widgets/widget-actions/WidgetActions.js.map +1 -1
  119. package/dist/bigconsole/components/workflow/WorkflowCard.js +2 -2
  120. package/dist/bigconsole/components/workflow/WorkflowCard.js.map +1 -1
  121. package/dist/bigconsole/components/workflow/WorkflowDeleteDialog.js +1 -1
  122. package/dist/bigconsole/components/workflow/WorkflowDeleteDialog.js.map +1 -1
  123. package/dist/bigconsole/pages/DashboardBuilderPage.js +2 -2
  124. package/dist/bigconsole/pages/DashboardBuilderPage.js.map +1 -1
  125. package/dist/bigconsole/pages/DashboardViewPage.js +1 -1
  126. package/dist/bigconsole/pages/DashboardViewPage.js.map +1 -1
  127. package/dist/bigconsole/pages/DashboardsPage.js +2 -2
  128. package/dist/bigconsole/pages/DashboardsPage.js.map +1 -1
  129. package/dist/bigconsole/pages/DataSinkBuilderPage.js +3 -3
  130. package/dist/bigconsole/pages/DataSinkBuilderPage.js.map +1 -1
  131. package/dist/bigconsole/pages/DataSinkViewPage.js +2 -2
  132. package/dist/bigconsole/pages/DataSinkViewPage.js.map +1 -1
  133. package/dist/bigconsole/pages/ParserBuilderPage.js +5 -5
  134. package/dist/bigconsole/pages/ParserBuilderPage.js.map +1 -1
  135. package/dist/bigconsole/pages/ParserViewPage.js +1 -1
  136. package/dist/bigconsole/pages/ParserViewPage.js.map +1 -1
  137. package/dist/bigconsole/pages/PipelineBuilderPage.js +4 -4
  138. package/dist/bigconsole/pages/PipelineBuilderPage.js.map +1 -1
  139. package/dist/bigconsole/pages/PipelineViewPage.js +9 -9
  140. package/dist/bigconsole/pages/PipelineViewPage.js.map +1 -1
  141. package/dist/bigconsole/pages/WidgetDemoPage.js +2 -2
  142. package/dist/bigconsole/pages/WidgetDemoPage.js.map +1 -1
  143. package/dist/bigconsole/pages/WorkflowViewPage.js +5 -5
  144. package/dist/bigconsole/pages/WorkflowViewPage.js.map +1 -1
  145. package/dist/bigconsole/renderers/adapters/AdaptiveCardRenderer.js +12 -12
  146. package/dist/bigconsole/renderers/adapters/AdaptiveCardRenderer.js.map +1 -1
  147. package/dist/bigconsole/renderers/adapters/BigConsoleRenderer.js +2 -2
  148. package/dist/bigconsole/renderers/adapters/BigConsoleRenderer.js.map +1 -1
  149. package/dist/bigconsole/renderers/adapters/BlockKitRenderer.js +5 -5
  150. package/dist/bigconsole/renderers/adapters/BlockKitRenderer.js.map +1 -1
  151. package/dist/bigconsole/renderers/adapters/CustomRenderer.js +2 -2
  152. package/dist/bigconsole/renderers/adapters/CustomRenderer.js.map +1 -1
  153. package/package.json +1 -1
@@ -121,7 +121,7 @@ var m = r(function({ widget: r, data: s, render: c, onClick: l }) {
121
121
  if (h && "value" in h) {
122
122
  let e = h.value, t = h.label || r.title, n = h.data;
123
123
  return /* @__PURE__ */ o("div", {
124
- className: `flex flex-col items-center justify-center h-full p-4 ${m ? "cursor-pointer hover:bg-bg-muted/50 transition-colors" : ""}`,
124
+ className: `flex flex-col items-center justify-center h-full p-4 ${m ? "cursor-pointer hover:bg-bg-sunken/50 transition-colors" : ""}`,
125
125
  onClick: l,
126
126
  children: [
127
127
  /* @__PURE__ */ a("div", {
@@ -135,7 +135,7 @@ var m = r(function({ widget: r, data: s, render: c, onClick: l }) {
135
135
  n && /* @__PURE__ */ a("div", {
136
136
  className: "mt-3 w-full grid grid-cols-2 gap-2 text-xs text-text-secondary",
137
137
  children: Object.entries(n).filter(([e]) => e !== "value" && e !== "label" && typeof n[e] != "object").slice(0, 4).map(([e, t]) => /* @__PURE__ */ o("div", {
138
- className: "flex justify-between px-2 py-1 bg-bg-muted rounded",
138
+ className: "flex justify-between px-2 py-1 bg-bg-sunken rounded",
139
139
  children: [/* @__PURE__ */ a("span", {
140
140
  className: "capitalize",
141
141
  children: e.replace(/([A-Z])/g, " $1").trim()
@@ -151,12 +151,12 @@ var m = r(function({ widget: r, data: s, render: c, onClick: l }) {
151
151
  return /* @__PURE__ */ o("div", {
152
152
  className: `
153
153
  flex flex-col items-center justify-center h-full p-4
154
- ${m ? "cursor-pointer hover:bg-bg-muted/50 transition-colors" : ""}
154
+ ${m ? "cursor-pointer hover:bg-bg-sunken/50 transition-colors" : ""}
155
155
  `,
156
156
  onClick: l,
157
157
  children: [
158
158
  /* @__PURE__ */ a("div", {
159
- className: "w-16 h-16 mb-4 flex items-center justify-center rounded-lg bg-bg-muted",
159
+ className: "w-16 h-16 mb-4 flex items-center justify-center rounded-lg bg-bg-sunken",
160
160
  children: /* @__PURE__ */ a("svg", {
161
161
  className: "w-8 h-8 text-text-secondary",
162
162
  fill: "none",
@@ -188,7 +188,7 @@ var m = r(function({ widget: r, data: s, render: c, onClick: l }) {
188
188
  s ? /* @__PURE__ */ a("div", {
189
189
  className: "mt-4 w-full max-h-[100px] overflow-auto",
190
190
  children: /* @__PURE__ */ a("pre", {
191
- className: "text-xs text-text-secondary bg-bg-muted p-2 rounded",
191
+ className: "text-xs text-text-secondary bg-bg-sunken p-2 rounded",
192
192
  children: JSON.stringify(s, null, 2)
193
193
  })
194
194
  }) : null
@@ -1 +1 @@
1
- {"version":3,"file":"CustomWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/custom/CustomWidget.tsx"],"sourcesContent":["/**\n * CustomWidget Component\n *\n * Supports callback-based rendering plus built-in HTML/iframe custom content.\n */\n\nimport { type FC, memo, type ReactNode, useMemo } from 'react';\nimport type { Widget, CustomWidgetConfig } from '../../../types';\nimport { getCustomConfigContent } from './config';\nimport { asRecord, asNonEmptyString } from './utils';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CustomWidgetProps {\n widget: Widget;\n data?: unknown;\n /** Custom renderer function */\n render?: (widget: Widget, data: unknown) => ReactNode;\n onClick?: () => void;\n}\n\nconst DEFAULT_IFRAME_SANDBOX = 'allow-scripts';\nconst ALLOWED_SANDBOX_TOKENS = new Set([\n 'allow-forms',\n 'allow-modals',\n 'allow-orientation-lock',\n 'allow-pointer-lock',\n 'allow-popups',\n 'allow-popups-to-escape-sandbox',\n 'allow-presentation',\n 'allow-same-origin',\n 'allow-scripts',\n 'allow-storage-access-by-user-activation',\n]);\n\nfunction isSafeDomUrl(rawUrl: string): boolean {\n const candidate = rawUrl.trim();\n if (!candidate) return false;\n if (candidate.startsWith('#')) return true;\n if (candidate.startsWith('/')) return !candidate.startsWith('//');\n\n try {\n const parsed = new URL(candidate, window.location.origin);\n return parsed.protocol === 'http:' || parsed.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\nfunction escapeHtml(input: string): string {\n return input\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&#39;');\n}\n\nfunction sanitizeHtml(input: string): string {\n if (typeof window === 'undefined') {\n return escapeHtml(input);\n }\n\n const parser = new DOMParser();\n const doc = parser.parseFromString(input, 'text/html');\n\n doc.body.querySelectorAll('script, style, iframe, object, embed, link, meta, base, form').forEach((el) => {\n el.remove();\n });\n\n doc.body.querySelectorAll('*').forEach((el) => {\n Array.from(el.attributes).forEach((attr) => {\n const attrName = attr.name.toLowerCase();\n const attrValue = attr.value.trim();\n\n if (attrName.startsWith('on')) {\n el.removeAttribute(attr.name);\n return;\n }\n\n if (attrName === 'style') {\n el.removeAttribute(attr.name);\n return;\n }\n\n if (['src', 'href', 'xlink:href'].includes(attrName) && !isSafeDomUrl(attrValue)) {\n el.removeAttribute(attr.name);\n }\n });\n });\n\n return doc.body.innerHTML;\n}\n\nfunction sanitizeIframeUrl(rawUrl: string): string | null {\n const candidate = rawUrl.trim();\n if (!candidate) return null;\n if (candidate.startsWith('//')) return null;\n if (candidate.startsWith('/')) return candidate;\n\n try {\n const parsed = new URL(candidate);\n if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {\n return parsed.href;\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nfunction sanitizeSandbox(rawSandbox: string | null): string {\n if (!rawSandbox) return DEFAULT_IFRAME_SANDBOX;\n\n const uniqueTokens = new Set(rawSandbox.split(/\\s+/).filter(Boolean));\n const safeTokens = Array.from(uniqueTokens).filter((token) => ALLOWED_SANDBOX_TOKENS.has(token));\n\n const hasScripts = safeTokens.includes('allow-scripts');\n const hasSameOrigin = safeTokens.includes('allow-same-origin');\n const withoutRiskyCombo =\n hasScripts && hasSameOrigin ? safeTokens.filter((t) => t !== 'allow-same-origin') : safeTokens;\n\n return withoutRiskyCombo.length > 0 ? withoutRiskyCombo.join(' ') : DEFAULT_IFRAME_SANDBOX;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const CustomWidget: FC<CustomWidgetProps> = memo(function CustomWidget({ widget, data, render, onClick }) {\n const config = widget.config as unknown as CustomWidgetConfig | undefined;\n const isClickable = Boolean(onClick && widget.drilldowns?.length);\n const typedData = useMemo(() => asRecord(data), [data]);\n const configRecord = useMemo(() => asRecord(config), [config]);\n const configProps = useMemo(() => asRecord(configRecord?.props), [configRecord]);\n const configContent = useMemo(() => getCustomConfigContent(config), [config]);\n const componentId = configContent.componentId;\n\n const htmlContent = useMemo(() => {\n const rawHtml = asNonEmptyString(typedData?.html) ?? asNonEmptyString(typedData?.contentHtml) ?? configContent.html;\n\n if (!rawHtml) return null;\n\n const sanitized = sanitizeHtml(rawHtml);\n return sanitized.trim().length > 0 ? sanitized : null;\n }, [typedData, configContent]);\n\n const iframeContent = useMemo(() => {\n const rawUrl =\n asNonEmptyString(typedData?.iframeUrl) ?? asNonEmptyString(typedData?.url) ?? configContent.iframeUrl;\n\n if (!rawUrl) return null;\n\n const safeUrl = sanitizeIframeUrl(rawUrl);\n if (!safeUrl) return null;\n\n const title =\n asNonEmptyString(typedData?.title) ??\n asNonEmptyString(configProps?.iframeTitle) ??\n asNonEmptyString(configProps?.title) ??\n widget.title ??\n 'Custom widget content';\n\n const sandboxSource = asNonEmptyString(configRecord?.sandbox) ?? asNonEmptyString(configProps?.sandbox);\n const sandbox = sanitizeSandbox(sandboxSource);\n\n return {\n url: safeUrl,\n title,\n sandbox,\n };\n }, [typedData, configContent, configProps, configRecord, widget.title]);\n\n // If custom render function is provided, use it\n if (render) {\n return (\n <div\n className={`\n h-full\n ${isClickable ? 'cursor-pointer' : ''}\n `}\n onClick={onClick}\n >\n {render(widget, data)}\n </div>\n );\n }\n\n if (htmlContent) {\n return (\n <div className=\"h-full overflow-auto p-4\">\n <div\n className=\"prose prose-sm max-w-none text-text-primary\"\n dangerouslySetInnerHTML={{ __html: htmlContent }}\n />\n </div>\n );\n }\n\n if (iframeContent) {\n return (\n <div className=\"h-full w-full\">\n <iframe\n src={iframeContent.url}\n title={iframeContent.title}\n className=\"h-full w-full rounded border-0\"\n sandbox={iframeContent.sandbox}\n loading=\"lazy\"\n />\n </div>\n );\n }\n\n // If data has value/label shape (from WidgetWrapper normalization), render as KPI-style\n if (typedData && 'value' in typedData) {\n const value = typedData.value;\n const label = (typedData.label as string) || widget.title;\n const nestedData = typedData.data as Record<string, unknown> | undefined;\n return (\n <div\n className={`flex flex-col items-center justify-center h-full p-4 ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50 transition-colors' : ''}`}\n onClick={onClick}\n >\n <div className=\"text-4xl font-bold text-text-primary tabular-nums\">\n {typeof value === 'number' ? value.toLocaleString() : String(value)}\n </div>\n <div className=\"text-sm text-text-secondary mt-1\">{label}</div>\n {nestedData && (\n <div className=\"mt-3 w-full grid grid-cols-2 gap-2 text-xs text-text-secondary\">\n {Object.entries(nestedData)\n .filter(([k]) => k !== 'value' && k !== 'label' && typeof nestedData[k] !== 'object')\n .slice(0, 4)\n .map(([key, val]) => (\n <div key={key} className=\"flex justify-between px-2 py-1 bg-bg-muted rounded\">\n <span className=\"capitalize\">{key.replace(/([A-Z])/g, ' $1').trim()}</span>\n <span className=\"font-medium\">{typeof val === 'number' ? val.toLocaleString() : String(val)}</span>\n </div>\n ))}\n </div>\n )}\n </div>\n );\n }\n\n // Default placeholder\n return (\n <div\n className={`\n flex flex-col items-center justify-center h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50 transition-colors' : ''}\n `}\n onClick={onClick}\n >\n {/* Placeholder icon */}\n <div className=\"w-16 h-16 mb-4 flex items-center justify-center rounded-lg bg-bg-muted\">\n <svg className=\"w-8 h-8 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"\n />\n </svg>\n </div>\n\n <h4 className=\"text-sm font-medium text-text-primary mb-1\">Custom Widget</h4>\n\n <p className=\"text-xs text-text-secondary text-center max-w-[200px]\">\n {config?.description || 'Provide HTML, an iframe URL, or a registered custom component.'}\n </p>\n\n {componentId ? (\n <p className=\"text-xs text-text-secondary mt-2\">\n Component: <code className=\"font-mono\">{componentId}</code>\n </p>\n ) : null}\n\n {/* Data preview */}\n {data ? (\n <div className=\"mt-4 w-full max-h-[100px] overflow-auto\">\n <pre className=\"text-xs text-text-secondary bg-bg-muted p-2 rounded\">\n {JSON.stringify(data as Record<string, unknown>, null, 2)}\n </pre>\n </div>\n ) : null}\n </div>\n );\n});\n\nexport default CustomWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"CustomWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/custom/CustomWidget.tsx"],"sourcesContent":["/**\n * CustomWidget Component\n *\n * Supports callback-based rendering plus built-in HTML/iframe custom content.\n */\n\nimport { type FC, memo, type ReactNode, useMemo } from 'react';\nimport type { Widget, CustomWidgetConfig } from '../../../types';\nimport { getCustomConfigContent } from './config';\nimport { asRecord, asNonEmptyString } from './utils';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CustomWidgetProps {\n widget: Widget;\n data?: unknown;\n /** Custom renderer function */\n render?: (widget: Widget, data: unknown) => ReactNode;\n onClick?: () => void;\n}\n\nconst DEFAULT_IFRAME_SANDBOX = 'allow-scripts';\nconst ALLOWED_SANDBOX_TOKENS = new Set([\n 'allow-forms',\n 'allow-modals',\n 'allow-orientation-lock',\n 'allow-pointer-lock',\n 'allow-popups',\n 'allow-popups-to-escape-sandbox',\n 'allow-presentation',\n 'allow-same-origin',\n 'allow-scripts',\n 'allow-storage-access-by-user-activation',\n]);\n\nfunction isSafeDomUrl(rawUrl: string): boolean {\n const candidate = rawUrl.trim();\n if (!candidate) return false;\n if (candidate.startsWith('#')) return true;\n if (candidate.startsWith('/')) return !candidate.startsWith('//');\n\n try {\n const parsed = new URL(candidate, window.location.origin);\n return parsed.protocol === 'http:' || parsed.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\nfunction escapeHtml(input: string): string {\n return input\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;')\n .replaceAll(\"'\", '&#39;');\n}\n\nfunction sanitizeHtml(input: string): string {\n if (typeof window === 'undefined') {\n return escapeHtml(input);\n }\n\n const parser = new DOMParser();\n const doc = parser.parseFromString(input, 'text/html');\n\n doc.body.querySelectorAll('script, style, iframe, object, embed, link, meta, base, form').forEach((el) => {\n el.remove();\n });\n\n doc.body.querySelectorAll('*').forEach((el) => {\n Array.from(el.attributes).forEach((attr) => {\n const attrName = attr.name.toLowerCase();\n const attrValue = attr.value.trim();\n\n if (attrName.startsWith('on')) {\n el.removeAttribute(attr.name);\n return;\n }\n\n if (attrName === 'style') {\n el.removeAttribute(attr.name);\n return;\n }\n\n if (['src', 'href', 'xlink:href'].includes(attrName) && !isSafeDomUrl(attrValue)) {\n el.removeAttribute(attr.name);\n }\n });\n });\n\n return doc.body.innerHTML;\n}\n\nfunction sanitizeIframeUrl(rawUrl: string): string | null {\n const candidate = rawUrl.trim();\n if (!candidate) return null;\n if (candidate.startsWith('//')) return null;\n if (candidate.startsWith('/')) return candidate;\n\n try {\n const parsed = new URL(candidate);\n if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {\n return parsed.href;\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nfunction sanitizeSandbox(rawSandbox: string | null): string {\n if (!rawSandbox) return DEFAULT_IFRAME_SANDBOX;\n\n const uniqueTokens = new Set(rawSandbox.split(/\\s+/).filter(Boolean));\n const safeTokens = Array.from(uniqueTokens).filter((token) => ALLOWED_SANDBOX_TOKENS.has(token));\n\n const hasScripts = safeTokens.includes('allow-scripts');\n const hasSameOrigin = safeTokens.includes('allow-same-origin');\n const withoutRiskyCombo =\n hasScripts && hasSameOrigin ? safeTokens.filter((t) => t !== 'allow-same-origin') : safeTokens;\n\n return withoutRiskyCombo.length > 0 ? withoutRiskyCombo.join(' ') : DEFAULT_IFRAME_SANDBOX;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const CustomWidget: FC<CustomWidgetProps> = memo(function CustomWidget({ widget, data, render, onClick }) {\n const config = widget.config as unknown as CustomWidgetConfig | undefined;\n const isClickable = Boolean(onClick && widget.drilldowns?.length);\n const typedData = useMemo(() => asRecord(data), [data]);\n const configRecord = useMemo(() => asRecord(config), [config]);\n const configProps = useMemo(() => asRecord(configRecord?.props), [configRecord]);\n const configContent = useMemo(() => getCustomConfigContent(config), [config]);\n const componentId = configContent.componentId;\n\n const htmlContent = useMemo(() => {\n const rawHtml = asNonEmptyString(typedData?.html) ?? asNonEmptyString(typedData?.contentHtml) ?? configContent.html;\n\n if (!rawHtml) return null;\n\n const sanitized = sanitizeHtml(rawHtml);\n return sanitized.trim().length > 0 ? sanitized : null;\n }, [typedData, configContent]);\n\n const iframeContent = useMemo(() => {\n const rawUrl =\n asNonEmptyString(typedData?.iframeUrl) ?? asNonEmptyString(typedData?.url) ?? configContent.iframeUrl;\n\n if (!rawUrl) return null;\n\n const safeUrl = sanitizeIframeUrl(rawUrl);\n if (!safeUrl) return null;\n\n const title =\n asNonEmptyString(typedData?.title) ??\n asNonEmptyString(configProps?.iframeTitle) ??\n asNonEmptyString(configProps?.title) ??\n widget.title ??\n 'Custom widget content';\n\n const sandboxSource = asNonEmptyString(configRecord?.sandbox) ?? asNonEmptyString(configProps?.sandbox);\n const sandbox = sanitizeSandbox(sandboxSource);\n\n return {\n url: safeUrl,\n title,\n sandbox,\n };\n }, [typedData, configContent, configProps, configRecord, widget.title]);\n\n // If custom render function is provided, use it\n if (render) {\n return (\n <div\n className={`\n h-full\n ${isClickable ? 'cursor-pointer' : ''}\n `}\n onClick={onClick}\n >\n {render(widget, data)}\n </div>\n );\n }\n\n if (htmlContent) {\n return (\n <div className=\"h-full overflow-auto p-4\">\n <div\n className=\"prose prose-sm max-w-none text-text-primary\"\n dangerouslySetInnerHTML={{ __html: htmlContent }}\n />\n </div>\n );\n }\n\n if (iframeContent) {\n return (\n <div className=\"h-full w-full\">\n <iframe\n src={iframeContent.url}\n title={iframeContent.title}\n className=\"h-full w-full rounded border-0\"\n sandbox={iframeContent.sandbox}\n loading=\"lazy\"\n />\n </div>\n );\n }\n\n // If data has value/label shape (from WidgetWrapper normalization), render as KPI-style\n if (typedData && 'value' in typedData) {\n const value = typedData.value;\n const label = (typedData.label as string) || widget.title;\n const nestedData = typedData.data as Record<string, unknown> | undefined;\n return (\n <div\n className={`flex flex-col items-center justify-center h-full p-4 ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}`}\n onClick={onClick}\n >\n <div className=\"text-4xl font-bold text-text-primary tabular-nums\">\n {typeof value === 'number' ? value.toLocaleString() : String(value)}\n </div>\n <div className=\"text-sm text-text-secondary mt-1\">{label}</div>\n {nestedData && (\n <div className=\"mt-3 w-full grid grid-cols-2 gap-2 text-xs text-text-secondary\">\n {Object.entries(nestedData)\n .filter(([k]) => k !== 'value' && k !== 'label' && typeof nestedData[k] !== 'object')\n .slice(0, 4)\n .map(([key, val]) => (\n <div key={key} className=\"flex justify-between px-2 py-1 bg-bg-sunken rounded\">\n <span className=\"capitalize\">{key.replace(/([A-Z])/g, ' $1').trim()}</span>\n <span className=\"font-medium\">{typeof val === 'number' ? val.toLocaleString() : String(val)}</span>\n </div>\n ))}\n </div>\n )}\n </div>\n );\n }\n\n // Default placeholder\n return (\n <div\n className={`\n flex flex-col items-center justify-center h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n `}\n onClick={onClick}\n >\n {/* Placeholder icon */}\n <div className=\"w-16 h-16 mb-4 flex items-center justify-center rounded-lg bg-bg-sunken\">\n <svg className=\"w-8 h-8 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"\n />\n </svg>\n </div>\n\n <h4 className=\"text-sm font-medium text-text-primary mb-1\">Custom Widget</h4>\n\n <p className=\"text-xs text-text-secondary text-center max-w-[200px]\">\n {config?.description || 'Provide HTML, an iframe URL, or a registered custom component.'}\n </p>\n\n {componentId ? (\n <p className=\"text-xs text-text-secondary mt-2\">\n Component: <code className=\"font-mono\">{componentId}</code>\n </p>\n ) : null}\n\n {/* Data preview */}\n {data ? (\n <div className=\"mt-4 w-full max-h-[100px] overflow-auto\">\n <pre className=\"text-xs text-text-secondary bg-bg-sunken p-2 rounded\">\n {JSON.stringify(data as Record<string, unknown>, null, 2)}\n </pre>\n </div>\n ) : null}\n </div>\n );\n});\n\nexport default CustomWidget;\n"],"mappings":"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"}
@@ -140,7 +140,7 @@ var d = r(function({ widget: r, data: c, onClick: d, onDrilldown: f }) {
140
140
  return v ? /* @__PURE__ */ s("div", {
141
141
  className: `
142
142
  flex flex-col items-center justify-center h-full p-4
143
- ${b ? "cursor-pointer hover:bg-bg-muted/50 transition-colors" : ""}
143
+ ${b ? "cursor-pointer hover:bg-bg-sunken/50 transition-colors" : ""}
144
144
  `,
145
145
  onClick: y,
146
146
  role: b ? "button" : void 0,
@@ -1 +1 @@
1
- {"version":3,"file":"GaugeWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/gauge/GaugeWidget.tsx"],"sourcesContent":["/**\n * GaugeWidget Component\n *\n * Gauge widget with circular and linear variants.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, GaugeConfig } from '../../../types';\nimport { CircularGauge, type ThresholdConfig } from './CircularGauge';\nimport { LinearGauge } from './LinearGauge';\nimport { unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface GaugeWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Gauge data */\n data?: GaugeData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives gauge data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface GaugeData {\n /** Current value */\n value: number;\n /** Optional label */\n label?: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst DEFAULT_THRESHOLDS: ThresholdConfig[] = [\n { value: 33, color: 'var(--color-state-error)' },\n { value: 66, color: 'var(--color-state-warning)' },\n { value: 100, color: 'var(--color-state-success)' },\n];\n\nfunction buildThresholds(config?: GaugeConfig): ThresholdConfig[] {\n if (!config?.thresholds?.length) {\n return DEFAULT_THRESHOLDS;\n }\n\n return config.thresholds.map((t) => ({\n value: t.value,\n color:\n t.color === 'success'\n ? 'var(--color-state-success)'\n : t.color === 'warning'\n ? 'var(--color-state-warning)'\n : t.color === 'error'\n ? 'var(--color-state-error)'\n : t.color,\n }));\n}\n\nfunction formatGaugeValue(value: number, config?: GaugeConfig): string {\n const format = config?.format || 'number';\n\n switch (format) {\n case 'percentage':\n return `${value.toFixed(0)}%`;\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'number':\n default:\n return new Intl.NumberFormat('en-US', {\n maximumFractionDigits: config?.decimals || 0,\n }).format(value);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const GaugeWidget: FC<GaugeWidgetProps> = memo(function GaugeWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as GaugeConfig | undefined;\n\n const thresholds = useMemo(() => buildThresholds(config), [config]);\n\n const formatValue = useCallback((value: number) => formatGaugeValue(value, config), [config]);\n\n const hasDrilldown = !!widget.drilldown?.enabled;\n const isLinear = config?.variant === 'linear';\n\n // Normalize data from different formats (DataSink array, single object, etc.)\n const normalizedData = useMemo((): GaugeData | null => {\n if (!rawData) return null;\n\n // Explicit top-level `value` always wins — even if a sibling envelope\n // key exists (`{value: 42, data: [...]}` is a common summary+detail\n // shape) we prefer the summary value rather than coercing to array mode.\n if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {\n const asRecord = rawData as unknown as Record<string, unknown>;\n if ('value' in asRecord && typeof asRecord.value === 'number') {\n return rawData as GaugeData;\n }\n }\n\n // BOFF-2246: unwrap common envelopes so the existing array handler below\n // (with its rich common-field heuristics) runs for parsers emitting\n // `{data: [...]}` / `{items: [...]}` — previously those payloads hit\n // \"No gauge data\" because no handler matched.\n //\n // Caveat: summary+detail payloads like `{ totalSales: 120, data: [...] }`\n // should keep the top-level numeric (the existing single-object handler\n // below has a generic \"any numeric field\" last-resort scan that would\n // otherwise surface it). Skip the unwrap whenever ANY numeric sibling\n // exists alongside the envelope key — matches the scope of the generic\n // scan so we don't regress previously-working payloads.\n let data: unknown = rawData;\n if (data && typeof data === 'object' && !Array.isArray(data)) {\n const obj = data as Record<string, unknown>;\n const hasTopLevelNumericSibling = Object.entries(obj).some(([k, v]) => {\n if (k === 'data' || k === 'items' || k === 'rows') return false;\n return typeof v === 'number';\n });\n\n if (!hasTopLevelNumericSibling) {\n // Array-aware unwrap — picks the first envelope key that's actually\n // an array, so payloads like `{data: {page: 1}, items: [...]}` still\n // reach the array handler below.\n const maybe = unwrapRows(obj);\n if (maybe && maybe.length > 0) data = maybe;\n }\n }\n\n // Get configured value field from widget config\n const valueField =\n ((config as Record<string, unknown> | undefined)?.valueField as string) ||\n ((config as Record<string, unknown> | undefined)?.value_field as string) ||\n 'value';\n\n // Handle array from DataSink - extract value from first item\n if (Array.isArray(data) && data.length > 0) {\n const firstItem = data[0] as Record<string, unknown>;\n\n // Try configured field first\n if (typeof firstItem[valueField] === 'number') {\n return {\n value: firstItem[valueField] as number,\n label: typeof firstItem.label === 'string' ? firstItem.label : undefined,\n };\n }\n\n // Try common numeric field names\n const numericFields = [\n 'value',\n 'growth',\n 'percentage',\n 'percent',\n 'progress',\n 'score',\n 'rate',\n 'amount',\n 'total',\n ];\n for (const field of numericFields) {\n if (typeof firstItem[field] === 'number') {\n return {\n value: firstItem[field] as number,\n label:\n typeof firstItem.label === 'string'\n ? firstItem.label\n : typeof firstItem.name === 'string'\n ? firstItem.name\n : undefined,\n };\n }\n }\n\n // Find any numeric field\n const numericEntry = Object.entries(firstItem).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n return {\n value: numericEntry[1] as number,\n label: `${numericEntry[0].charAt(0).toUpperCase()}${numericEntry[0].slice(1)}`,\n };\n }\n\n // Try to aggregate (average) across all rows if single item didn't work\n if (data.length > 1) {\n const allNumericFields = Object.entries(firstItem).filter(([, v]) => typeof v === 'number');\n if (allNumericFields.length > 0) {\n const [fieldName] = allNumericFields[0];\n const avg =\n data.reduce((sum, row) => {\n const val = (row as Record<string, unknown>)[fieldName];\n return sum + (typeof val === 'number' ? val : 0);\n }, 0) / data.length;\n\n return {\n value: Math.round(avg * 100) / 100,\n label: `Avg ${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`,\n };\n }\n }\n }\n\n // Handle single object without 'value' property\n if (typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as unknown as Record<string, unknown>;\n\n // Try configured field\n if (typeof dataObj[valueField] === 'number') {\n return {\n value: dataObj[valueField] as number,\n label: typeof dataObj.label === 'string' ? dataObj.label : undefined,\n };\n }\n\n // Try common field names\n const numericFields = [\n 'value',\n 'growth',\n 'percentage',\n 'percent',\n 'progress',\n 'score',\n 'rate',\n 'amount',\n 'total',\n ];\n for (const field of numericFields) {\n if (typeof dataObj[field] === 'number') {\n return {\n value: dataObj[field] as number,\n label: typeof dataObj.label === 'string' ? dataObj.label : undefined,\n };\n }\n }\n\n // Find any numeric field\n const numericEntry = Object.entries(dataObj).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n return {\n value: numericEntry[1] as number,\n label: `${numericEntry[0].charAt(0).toUpperCase()}${numericEntry[0].slice(1)}`,\n };\n }\n }\n\n // Handle direct number\n if (typeof data === 'number') {\n return { value: data };\n }\n\n // Handle string that can be parsed as number\n if (typeof data === 'string') {\n const parsed = parseFloat(data);\n if (!isNaN(parsed)) {\n return { value: parsed };\n }\n }\n\n return null;\n }, [rawData, config]);\n\n // Handle drilldown click - must be after normalizedData is computed\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown && normalizedData) {\n onDrilldown({\n value: normalizedData.value,\n label: normalizedData.label,\n min: config?.min,\n max: config?.max,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, normalizedData, config?.min, config?.max]);\n\n const isClickable = Boolean(hasDrilldown || (onClick && widget.drilldowns?.length));\n\n if (!normalizedData) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No gauge data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={`\n flex flex-col items-center justify-center h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Label */}\n {normalizedData.label && <p className=\"text-sm text-text-secondary mb-3 text-center\">{normalizedData.label}</p>}\n\n {/* Gauge */}\n <div className={`${isLinear ? 'w-full max-w-xs' : ''}`}>\n {isLinear ? (\n <LinearGauge\n value={normalizedData.value}\n min={config?.min}\n max={config?.max}\n thresholds={thresholds}\n unit={config?.unit}\n formatValue={formatValue}\n showZones={config?.showZones}\n />\n ) : (\n <CircularGauge\n value={normalizedData.value}\n min={config?.min}\n max={config?.max}\n thresholds={thresholds}\n unit={config?.unit}\n formatValue={formatValue}\n size={config?.size === 'sm' ? 100 : config?.size === 'md' ? 140 : config?.size === 'lg' ? 180 : 140}\n />\n )}\n </div>\n\n {/* Drilldown indicator */}\n {isClickable && (\n <div className=\"mt-3 flex items-center justify-center\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default GaugeWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"GaugeWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/gauge/GaugeWidget.tsx"],"sourcesContent":["/**\n * GaugeWidget Component\n *\n * Gauge widget with circular and linear variants.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, GaugeConfig } from '../../../types';\nimport { CircularGauge, type ThresholdConfig } from './CircularGauge';\nimport { LinearGauge } from './LinearGauge';\nimport { unwrapRows } from '../utils/inferFields';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface GaugeWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Gauge data */\n data?: GaugeData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives gauge data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface GaugeData {\n /** Current value */\n value: number;\n /** Optional label */\n label?: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst DEFAULT_THRESHOLDS: ThresholdConfig[] = [\n { value: 33, color: 'var(--color-state-error)' },\n { value: 66, color: 'var(--color-state-warning)' },\n { value: 100, color: 'var(--color-state-success)' },\n];\n\nfunction buildThresholds(config?: GaugeConfig): ThresholdConfig[] {\n if (!config?.thresholds?.length) {\n return DEFAULT_THRESHOLDS;\n }\n\n return config.thresholds.map((t) => ({\n value: t.value,\n color:\n t.color === 'success'\n ? 'var(--color-state-success)'\n : t.color === 'warning'\n ? 'var(--color-state-warning)'\n : t.color === 'error'\n ? 'var(--color-state-error)'\n : t.color,\n }));\n}\n\nfunction formatGaugeValue(value: number, config?: GaugeConfig): string {\n const format = config?.format || 'number';\n\n switch (format) {\n case 'percentage':\n return `${value.toFixed(0)}%`;\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'number':\n default:\n return new Intl.NumberFormat('en-US', {\n maximumFractionDigits: config?.decimals || 0,\n }).format(value);\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const GaugeWidget: FC<GaugeWidgetProps> = memo(function GaugeWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as GaugeConfig | undefined;\n\n const thresholds = useMemo(() => buildThresholds(config), [config]);\n\n const formatValue = useCallback((value: number) => formatGaugeValue(value, config), [config]);\n\n const hasDrilldown = !!widget.drilldown?.enabled;\n const isLinear = config?.variant === 'linear';\n\n // Normalize data from different formats (DataSink array, single object, etc.)\n const normalizedData = useMemo((): GaugeData | null => {\n if (!rawData) return null;\n\n // Explicit top-level `value` always wins — even if a sibling envelope\n // key exists (`{value: 42, data: [...]}` is a common summary+detail\n // shape) we prefer the summary value rather than coercing to array mode.\n if (rawData && typeof rawData === 'object' && !Array.isArray(rawData)) {\n const asRecord = rawData as unknown as Record<string, unknown>;\n if ('value' in asRecord && typeof asRecord.value === 'number') {\n return rawData as GaugeData;\n }\n }\n\n // BOFF-2246: unwrap common envelopes so the existing array handler below\n // (with its rich common-field heuristics) runs for parsers emitting\n // `{data: [...]}` / `{items: [...]}` — previously those payloads hit\n // \"No gauge data\" because no handler matched.\n //\n // Caveat: summary+detail payloads like `{ totalSales: 120, data: [...] }`\n // should keep the top-level numeric (the existing single-object handler\n // below has a generic \"any numeric field\" last-resort scan that would\n // otherwise surface it). Skip the unwrap whenever ANY numeric sibling\n // exists alongside the envelope key — matches the scope of the generic\n // scan so we don't regress previously-working payloads.\n let data: unknown = rawData;\n if (data && typeof data === 'object' && !Array.isArray(data)) {\n const obj = data as Record<string, unknown>;\n const hasTopLevelNumericSibling = Object.entries(obj).some(([k, v]) => {\n if (k === 'data' || k === 'items' || k === 'rows') return false;\n return typeof v === 'number';\n });\n\n if (!hasTopLevelNumericSibling) {\n // Array-aware unwrap — picks the first envelope key that's actually\n // an array, so payloads like `{data: {page: 1}, items: [...]}` still\n // reach the array handler below.\n const maybe = unwrapRows(obj);\n if (maybe && maybe.length > 0) data = maybe;\n }\n }\n\n // Get configured value field from widget config\n const valueField =\n ((config as Record<string, unknown> | undefined)?.valueField as string) ||\n ((config as Record<string, unknown> | undefined)?.value_field as string) ||\n 'value';\n\n // Handle array from DataSink - extract value from first item\n if (Array.isArray(data) && data.length > 0) {\n const firstItem = data[0] as Record<string, unknown>;\n\n // Try configured field first\n if (typeof firstItem[valueField] === 'number') {\n return {\n value: firstItem[valueField] as number,\n label: typeof firstItem.label === 'string' ? firstItem.label : undefined,\n };\n }\n\n // Try common numeric field names\n const numericFields = [\n 'value',\n 'growth',\n 'percentage',\n 'percent',\n 'progress',\n 'score',\n 'rate',\n 'amount',\n 'total',\n ];\n for (const field of numericFields) {\n if (typeof firstItem[field] === 'number') {\n return {\n value: firstItem[field] as number,\n label:\n typeof firstItem.label === 'string'\n ? firstItem.label\n : typeof firstItem.name === 'string'\n ? firstItem.name\n : undefined,\n };\n }\n }\n\n // Find any numeric field\n const numericEntry = Object.entries(firstItem).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n return {\n value: numericEntry[1] as number,\n label: `${numericEntry[0].charAt(0).toUpperCase()}${numericEntry[0].slice(1)}`,\n };\n }\n\n // Try to aggregate (average) across all rows if single item didn't work\n if (data.length > 1) {\n const allNumericFields = Object.entries(firstItem).filter(([, v]) => typeof v === 'number');\n if (allNumericFields.length > 0) {\n const [fieldName] = allNumericFields[0];\n const avg =\n data.reduce((sum, row) => {\n const val = (row as Record<string, unknown>)[fieldName];\n return sum + (typeof val === 'number' ? val : 0);\n }, 0) / data.length;\n\n return {\n value: Math.round(avg * 100) / 100,\n label: `Avg ${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`,\n };\n }\n }\n }\n\n // Handle single object without 'value' property\n if (typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as unknown as Record<string, unknown>;\n\n // Try configured field\n if (typeof dataObj[valueField] === 'number') {\n return {\n value: dataObj[valueField] as number,\n label: typeof dataObj.label === 'string' ? dataObj.label : undefined,\n };\n }\n\n // Try common field names\n const numericFields = [\n 'value',\n 'growth',\n 'percentage',\n 'percent',\n 'progress',\n 'score',\n 'rate',\n 'amount',\n 'total',\n ];\n for (const field of numericFields) {\n if (typeof dataObj[field] === 'number') {\n return {\n value: dataObj[field] as number,\n label: typeof dataObj.label === 'string' ? dataObj.label : undefined,\n };\n }\n }\n\n // Find any numeric field\n const numericEntry = Object.entries(dataObj).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n return {\n value: numericEntry[1] as number,\n label: `${numericEntry[0].charAt(0).toUpperCase()}${numericEntry[0].slice(1)}`,\n };\n }\n }\n\n // Handle direct number\n if (typeof data === 'number') {\n return { value: data };\n }\n\n // Handle string that can be parsed as number\n if (typeof data === 'string') {\n const parsed = parseFloat(data);\n if (!isNaN(parsed)) {\n return { value: parsed };\n }\n }\n\n return null;\n }, [rawData, config]);\n\n // Handle drilldown click - must be after normalizedData is computed\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown && normalizedData) {\n onDrilldown({\n value: normalizedData.value,\n label: normalizedData.label,\n min: config?.min,\n max: config?.max,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, normalizedData, config?.min, config?.max]);\n\n const isClickable = Boolean(hasDrilldown || (onClick && widget.drilldowns?.length));\n\n if (!normalizedData) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No gauge data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div\n className={`\n flex flex-col items-center justify-center h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Label */}\n {normalizedData.label && <p className=\"text-sm text-text-secondary mb-3 text-center\">{normalizedData.label}</p>}\n\n {/* Gauge */}\n <div className={`${isLinear ? 'w-full max-w-xs' : ''}`}>\n {isLinear ? (\n <LinearGauge\n value={normalizedData.value}\n min={config?.min}\n max={config?.max}\n thresholds={thresholds}\n unit={config?.unit}\n formatValue={formatValue}\n showZones={config?.showZones}\n />\n ) : (\n <CircularGauge\n value={normalizedData.value}\n min={config?.min}\n max={config?.max}\n thresholds={thresholds}\n unit={config?.unit}\n formatValue={formatValue}\n size={config?.size === 'sm' ? 100 : config?.size === 'md' ? 140 : config?.size === 'lg' ? 180 : 140}\n />\n )}\n </div>\n\n {/* Drilldown indicator */}\n {isClickable && (\n <div className=\"mt-3 flex items-center justify-center\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default GaugeWidget;\n"],"mappings":"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"}
@@ -44,7 +44,7 @@ var a = e(function({ value: e, min: a = 0, max: o = 100, height: s = 12, thresho
44
44
  })]
45
45
  }),
46
46
  /* @__PURE__ */ r("div", {
47
- className: "relative w-full rounded-full overflow-hidden bg-bg-muted",
47
+ className: "relative w-full rounded-full overflow-hidden bg-bg-sunken",
48
48
  style: { height: s },
49
49
  children: [
50
50
  g && /* @__PURE__ */ n("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"LinearGauge.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/gauge/LinearGauge.tsx"],"sourcesContent":["/**\n * LinearGauge Component\n *\n * Horizontal linear gauge visualization.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { ThresholdConfig } from './CircularGauge';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface LinearGaugeProps {\n /** Current value */\n value: number;\n /** Minimum value */\n min?: number;\n /** Maximum value */\n max?: number;\n /** Height of the gauge bar */\n height?: number;\n /** Thresholds for color zones */\n thresholds?: ThresholdConfig[];\n /** Unit label */\n unit?: string;\n /** Whether to show the value */\n showValue?: boolean;\n /** Whether to show min/max labels */\n showRange?: boolean;\n /** Value format function */\n formatValue?: (value: number) => string;\n /** Whether to show color zones */\n showZones?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getColorForValue(value: number, thresholds?: ThresholdConfig[]): string {\n if (!thresholds?.length) {\n return 'var(--color-action-primary-bg)';\n }\n\n const sorted = [...thresholds].sort((a, b) => a.value - b.value);\n\n for (const threshold of sorted) {\n if (value <= threshold.value) {\n return threshold.color;\n }\n }\n\n return sorted[sorted.length - 1].color;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const LinearGauge: FC<LinearGaugeProps> = memo(function LinearGauge({\n value,\n min = 0,\n max = 100,\n height = 12,\n thresholds,\n unit,\n showValue = true,\n showRange = true,\n formatValue = (v: number) => v.toFixed(0),\n showZones = false,\n}) {\n // Calculate percentage\n const percentage = useMemo(() => {\n const clampedValue = Math.max(min, Math.min(max, value));\n return ((clampedValue - min) / (max - min)) * 100;\n }, [value, min, max]);\n\n const color = getColorForValue(value, thresholds);\n\n // Calculate zone widths for background\n const zones = useMemo(() => {\n if (!showZones || !thresholds?.length) return null;\n\n const sorted = [...thresholds].sort((a, b) => a.value - b.value);\n const zones: { width: number; color: string }[] = [];\n let prevValue = min;\n\n for (const threshold of sorted) {\n const width = ((threshold.value - prevValue) / (max - min)) * 100;\n zones.push({ width, color: threshold.color });\n prevValue = threshold.value;\n }\n\n return zones;\n }, [showZones, thresholds, min, max]);\n\n return (\n <div className=\"w-full\">\n {/* Value display */}\n {showValue && (\n <div className=\"flex items-baseline gap-1 mb-2\">\n <span className=\"text-2xl font-bold\" style={{ color }}>\n {formatValue(value)}\n </span>\n {unit && <span className=\"text-sm text-text-secondary\">{unit}</span>}\n </div>\n )}\n\n {/* Gauge bar */}\n <div className=\"relative w-full rounded-full overflow-hidden bg-bg-muted\" style={{ height }}>\n {/* Zone backgrounds */}\n {zones && (\n <div className=\"absolute inset-0 flex\">\n {zones.map((zone, index) => (\n <div\n key={index}\n className=\"h-full opacity-20\"\n style={{ width: `${zone.width}%`, backgroundColor: zone.color }}\n />\n ))}\n </div>\n )}\n\n {/* Value bar */}\n <div\n className=\"absolute inset-y-0 left-0 rounded-full transition-all duration-500 ease-out\"\n style={{\n width: `${percentage}%`,\n backgroundColor: color,\n }}\n />\n\n {/* Value marker */}\n <div\n className=\"absolute top-1/2 -translate-y-1/2 w-1 bg-bg-surface transition-all duration-500\"\n style={{\n left: `calc(${percentage}% - 2px)`,\n height: height + 4,\n }}\n />\n </div>\n\n {/* Range labels */}\n {showRange && (\n <div className=\"flex justify-between mt-1\">\n <span className=\"text-xs text-text-secondary\">{formatValue(min)}</span>\n <span className=\"text-xs text-text-secondary\">{formatValue(max)}</span>\n </div>\n )}\n </div>\n );\n});\n\nexport default LinearGauge;\n"],"mappings":";;;AAwCA,SAAS,EAAiB,GAAe,GAAwC;AAC/E,KAAI,CAAC,GAAY,OACf,QAAO;CAGT,IAAM,IAAS,CAAC,GAAG,EAAW,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;AAEhE,MAAK,IAAM,KAAa,EACtB,KAAI,KAAS,EAAU,MACrB,QAAO,EAAU;AAIrB,QAAO,EAAO,EAAO,SAAS,GAAG;;AAOnC,IAAa,IAAoC,EAAK,SAAqB,EACzE,UACA,SAAM,GACN,SAAM,KACN,YAAS,IACT,eACA,SACA,eAAY,IACZ,eAAY,IACZ,kBAAe,MAAc,EAAE,QAAQ,EAAE,EACzC,eAAY,MACX;CAED,IAAM,IAAa,SACI,KAAK,IAAI,GAAK,KAAK,IAAI,GAAK,EAAM,CAAC,GAChC,MAAQ,IAAM,KAAQ,KAC7C;EAAC;EAAO;EAAK;EAAI,CAAC,EAEf,IAAQ,EAAiB,GAAO,EAAW,EAG3C,IAAQ,QAAc;AAC1B,MAAI,CAAC,KAAa,CAAC,GAAY,OAAQ,QAAO;EAE9C,IAAM,IAAS,CAAC,GAAG,EAAW,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EAC1D,IAA4C,EAAE,EAChD,IAAY;AAEhB,OAAK,IAAM,KAAa,GAAQ;GAC9B,IAAM,KAAU,EAAU,QAAQ,MAAc,IAAM,KAAQ;AAE9D,GADA,EAAM,KAAK;IAAE;IAAO,OAAO,EAAU;IAAO,CAAC,EAC7C,IAAY,EAAU;;AAGxB,SAAO;IACN;EAAC;EAAW;EAAY;EAAK;EAAI,CAAC;AAErC,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEG,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;KAAqB,OAAO,EAAE,UAAO;eAClD,EAAY,EAAM;KACd,CAAA,EACN,KAAQ,kBAAC,QAAD;KAAM,WAAU;eAA+B;KAAY,CAAA,CAChE;;GAIR,kBAAC,OAAD;IAAK,WAAU;IAA2D,OAAO,EAAE,WAAQ;cAA3F;KAEG,KACC,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAM,KAAK,GAAM,MAChB,kBAAC,OAAD;OAEE,WAAU;OACV,OAAO;QAAE,OAAO,GAAG,EAAK,MAAM;QAAI,iBAAiB,EAAK;QAAO;OAC/D,EAHK,EAGL,CACF;MACE,CAAA;KAIR,kBAAC,OAAD;MACE,WAAU;MACV,OAAO;OACL,OAAO,GAAG,EAAW;OACrB,iBAAiB;OAClB;MACD,CAAA;KAGF,kBAAC,OAAD;MACE,WAAU;MACV,OAAO;OACL,MAAM,QAAQ,EAAW;OACzB,QAAQ,IAAS;OAClB;MACD,CAAA;KACE;;GAGL,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAA+B,EAAY,EAAI;KAAQ,CAAA,EACvE,kBAAC,QAAD;KAAM,WAAU;eAA+B,EAAY,EAAI;KAAQ,CAAA,CACnE;;GAEJ;;EAER"}
1
+ {"version":3,"file":"LinearGauge.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/gauge/LinearGauge.tsx"],"sourcesContent":["/**\n * LinearGauge Component\n *\n * Horizontal linear gauge visualization.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { ThresholdConfig } from './CircularGauge';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface LinearGaugeProps {\n /** Current value */\n value: number;\n /** Minimum value */\n min?: number;\n /** Maximum value */\n max?: number;\n /** Height of the gauge bar */\n height?: number;\n /** Thresholds for color zones */\n thresholds?: ThresholdConfig[];\n /** Unit label */\n unit?: string;\n /** Whether to show the value */\n showValue?: boolean;\n /** Whether to show min/max labels */\n showRange?: boolean;\n /** Value format function */\n formatValue?: (value: number) => string;\n /** Whether to show color zones */\n showZones?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getColorForValue(value: number, thresholds?: ThresholdConfig[]): string {\n if (!thresholds?.length) {\n return 'var(--color-action-primary-bg)';\n }\n\n const sorted = [...thresholds].sort((a, b) => a.value - b.value);\n\n for (const threshold of sorted) {\n if (value <= threshold.value) {\n return threshold.color;\n }\n }\n\n return sorted[sorted.length - 1].color;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const LinearGauge: FC<LinearGaugeProps> = memo(function LinearGauge({\n value,\n min = 0,\n max = 100,\n height = 12,\n thresholds,\n unit,\n showValue = true,\n showRange = true,\n formatValue = (v: number) => v.toFixed(0),\n showZones = false,\n}) {\n // Calculate percentage\n const percentage = useMemo(() => {\n const clampedValue = Math.max(min, Math.min(max, value));\n return ((clampedValue - min) / (max - min)) * 100;\n }, [value, min, max]);\n\n const color = getColorForValue(value, thresholds);\n\n // Calculate zone widths for background\n const zones = useMemo(() => {\n if (!showZones || !thresholds?.length) return null;\n\n const sorted = [...thresholds].sort((a, b) => a.value - b.value);\n const zones: { width: number; color: string }[] = [];\n let prevValue = min;\n\n for (const threshold of sorted) {\n const width = ((threshold.value - prevValue) / (max - min)) * 100;\n zones.push({ width, color: threshold.color });\n prevValue = threshold.value;\n }\n\n return zones;\n }, [showZones, thresholds, min, max]);\n\n return (\n <div className=\"w-full\">\n {/* Value display */}\n {showValue && (\n <div className=\"flex items-baseline gap-1 mb-2\">\n <span className=\"text-2xl font-bold\" style={{ color }}>\n {formatValue(value)}\n </span>\n {unit && <span className=\"text-sm text-text-secondary\">{unit}</span>}\n </div>\n )}\n\n {/* Gauge bar */}\n <div className=\"relative w-full rounded-full overflow-hidden bg-bg-sunken\" style={{ height }}>\n {/* Zone backgrounds */}\n {zones && (\n <div className=\"absolute inset-0 flex\">\n {zones.map((zone, index) => (\n <div\n key={index}\n className=\"h-full opacity-20\"\n style={{ width: `${zone.width}%`, backgroundColor: zone.color }}\n />\n ))}\n </div>\n )}\n\n {/* Value bar */}\n <div\n className=\"absolute inset-y-0 left-0 rounded-full transition-all duration-500 ease-out\"\n style={{\n width: `${percentage}%`,\n backgroundColor: color,\n }}\n />\n\n {/* Value marker */}\n <div\n className=\"absolute top-1/2 -translate-y-1/2 w-1 bg-bg-surface transition-all duration-500\"\n style={{\n left: `calc(${percentage}% - 2px)`,\n height: height + 4,\n }}\n />\n </div>\n\n {/* Range labels */}\n {showRange && (\n <div className=\"flex justify-between mt-1\">\n <span className=\"text-xs text-text-secondary\">{formatValue(min)}</span>\n <span className=\"text-xs text-text-secondary\">{formatValue(max)}</span>\n </div>\n )}\n </div>\n );\n});\n\nexport default LinearGauge;\n"],"mappings":";;;AAwCA,SAAS,EAAiB,GAAe,GAAwC;AAC/E,KAAI,CAAC,GAAY,OACf,QAAO;CAGT,IAAM,IAAS,CAAC,GAAG,EAAW,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;AAEhE,MAAK,IAAM,KAAa,EACtB,KAAI,KAAS,EAAU,MACrB,QAAO,EAAU;AAIrB,QAAO,EAAO,EAAO,SAAS,GAAG;;AAOnC,IAAa,IAAoC,EAAK,SAAqB,EACzE,UACA,SAAM,GACN,SAAM,KACN,YAAS,IACT,eACA,SACA,eAAY,IACZ,eAAY,IACZ,kBAAe,MAAc,EAAE,QAAQ,EAAE,EACzC,eAAY,MACX;CAED,IAAM,IAAa,SACI,KAAK,IAAI,GAAK,KAAK,IAAI,GAAK,EAAM,CAAC,GAChC,MAAQ,IAAM,KAAQ,KAC7C;EAAC;EAAO;EAAK;EAAI,CAAC,EAEf,IAAQ,EAAiB,GAAO,EAAW,EAG3C,IAAQ,QAAc;AAC1B,MAAI,CAAC,KAAa,CAAC,GAAY,OAAQ,QAAO;EAE9C,IAAM,IAAS,CAAC,GAAG,EAAW,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EAC1D,IAA4C,EAAE,EAChD,IAAY;AAEhB,OAAK,IAAM,KAAa,GAAQ;GAC9B,IAAM,KAAU,EAAU,QAAQ,MAAc,IAAM,KAAQ;AAE9D,GADA,EAAM,KAAK;IAAE;IAAO,OAAO,EAAU;IAAO,CAAC,EAC7C,IAAY,EAAU;;AAGxB,SAAO;IACN;EAAC;EAAW;EAAY;EAAK;EAAI,CAAC;AAErC,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEG,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;KAAqB,OAAO,EAAE,UAAO;eAClD,EAAY,EAAM;KACd,CAAA,EACN,KAAQ,kBAAC,QAAD;KAAM,WAAU;eAA+B;KAAY,CAAA,CAChE;;GAIR,kBAAC,OAAD;IAAK,WAAU;IAA4D,OAAO,EAAE,WAAQ;cAA5F;KAEG,KACC,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAM,KAAK,GAAM,MAChB,kBAAC,OAAD;OAEE,WAAU;OACV,OAAO;QAAE,OAAO,GAAG,EAAK,MAAM;QAAI,iBAAiB,EAAK;QAAO;OAC/D,EAHK,EAGL,CACF;MACE,CAAA;KAIR,kBAAC,OAAD;MACE,WAAU;MACV,OAAO;OACL,OAAO,GAAG,EAAW;OACrB,iBAAiB;OAClB;MACD,CAAA;KAGF,kBAAC,OAAD;MACE,WAAU;MACV,OAAO;OACL,MAAM,QAAQ,EAAW;OACzB,QAAQ,IAAS;OAClB;MACD,CAAA;KACE;;GAGL,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAA+B,EAAY,EAAI;KAAQ,CAAA,EACvE,kBAAC,QAAD;KAAM,WAAU;eAA+B,EAAY,EAAI;KAAQ,CAAA,CACnE;;GAEJ;;EAER"}
@@ -6,7 +6,7 @@ function i(e) {
6
6
  case "high": return "bg-state-error/20 text-state-error";
7
7
  case "medium": return "bg-state-warning/20 text-state-warning";
8
8
  case "low": return "bg-state-success/20 text-state-success";
9
- default: return "bg-bg-muted text-text-secondary";
9
+ default: return "bg-bg-sunken text-text-secondary";
10
10
  }
11
11
  }
12
12
  var a = e(function({ widget: e, data: a, onClick: o }) {
@@ -21,7 +21,7 @@ var a = e(function({ widget: e, data: a, onClick: o }) {
21
21
  children: s.map((e) => {
22
22
  let t = l[e.id] || [];
23
23
  return /* @__PURE__ */ r("div", {
24
- className: "flex-shrink-0 w-48 flex flex-col bg-bg-muted/50 rounded-lg",
24
+ className: "flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg",
25
25
  children: [/* @__PURE__ */ r("div", {
26
26
  className: "flex items-center gap-2 p-2 border-b border-border-default",
27
27
  children: [
@@ -1 +1 @@
1
- {"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-state-error/20 text-state-error';\n case 'medium':\n return 'bg-state-warning/20 text-state-warning';\n case 'low':\n return 'bg-state-success/20 text-state-success';\n default:\n return 'bg-bg-muted text-text-secondary';\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget: _widget, data, onClick }) {\n const columns = useMemo(() => data?.columns || [], [data?.columns]);\n const tasks = useMemo(() => data?.tasks || [], [data?.tasks]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>No board data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-muted/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-tertiary\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && <p className=\"text-[10px] text-text-tertiary text-center py-4\">No tasks</p>}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":";;;AA4CA,SAAS,EAAiB,GAA2B;AACnD,SAAQ,GAAR;EACE,KAAK,OACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,MACH,QAAO;EACT,QACE,QAAO;;;AAQb,IAAa,IAAsC,EAAK,SAAsB,EAAE,QAAQ,GAAS,SAAM,cAAW;CAChH,IAAM,IAAU,QAAc,GAAM,WAAW,EAAE,EAAE,CAAC,GAAM,QAAQ,CAAC,EAC7D,IAAQ,QAAc,GAAM,SAAS,EAAE,EAAE,CAAC,GAAM,MAAM,CAAC,EAGvD,IAAgB,QAAc;EAClC,IAAM,IAAwC,EAAE;AAIhD,SAHA,EAAQ,SAAS,MAAQ;AACvB,KAAQ,EAAI,MAAM,EAAM,QAAQ,MAAM,EAAE,WAAW,EAAI,GAAG;IAC1D,EACK;IACN,CAAC,GAAS,EAAM,CAAC;AAyBpB,QAvBK,EAAQ,SAwBX,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAQ,KAAK,MAAW;GACvB,IAAM,IAAc,EAAc,EAAO,OAAO,EAAE;AAElD,UACE,kBAAC,OAAD;IAAqB,WAAU;cAA/B,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,iBAAiB,EAAO,SAAS,2BAA2B;OACrE,CAAA;MACF,kBAAC,QAAD;OAAM,WAAU;iBAAkD,EAAO;OAAa,CAAA;MACtF,kBAAC,QAAD;OAAM,WAAU;iBAAsC,EAAY;OAAc,CAAA;MAC5E;QAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAY,KAAK,MAChB,kBAAC,OAAD;MAEE,WAAW;;wBAEL,IAAU,kDAAkD,GAAG;;;MAGrE,eAAe,IAAU,EAAK;gBAPhC,CASE,kBAAC,KAAD;OAAG,WAAU;iBAAsD,EAAK;OAAU,CAAA,EAElF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAW,qCAAqC,EAAiB,EAAK,SAAS;kBAClF,EAAK;QACD,CAAA,EAER,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAK;QACD,CAAA,CAEL;SACF;QAtBC,EAAK,GAsBN,CACN,EAED,EAAY,WAAW,KAAK,kBAAC,KAAD;MAAG,WAAU;gBAAkD;MAAY,CAAA,CACpG;OACF;MA1CI,EAAO,GA0CX;IAER;EACE,CAAA,GAxEJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,QAAO;IACP,SAAQ;cAER,kBAAC,QAAD;KACE,eAAc;KACd,gBAAe;KACf,aAAa;KACb,GAAE;KACF,CAAA;IACE,CAAA,EACN,kBAAC,KAAD,EAAA,UAAG,iBAAiB,CAAA,CAChB;;EACF,CAAA;EAyDV"}
1
+ {"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-state-error/20 text-state-error';\n case 'medium':\n return 'bg-state-warning/20 text-state-warning';\n case 'low':\n return 'bg-state-success/20 text-state-success';\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget: _widget, data, onClick }) {\n const columns = useMemo(() => data?.columns || [], [data?.columns]);\n const tasks = useMemo(() => data?.tasks || [], [data?.tasks]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>No board data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-tertiary\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && <p className=\"text-[10px] text-text-tertiary text-center py-4\">No tasks</p>}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":";;;AA4CA,SAAS,EAAiB,GAA2B;AACnD,SAAQ,GAAR;EACE,KAAK,OACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,MACH,QAAO;EACT,QACE,QAAO;;;AAQb,IAAa,IAAsC,EAAK,SAAsB,EAAE,QAAQ,GAAS,SAAM,cAAW;CAChH,IAAM,IAAU,QAAc,GAAM,WAAW,EAAE,EAAE,CAAC,GAAM,QAAQ,CAAC,EAC7D,IAAQ,QAAc,GAAM,SAAS,EAAE,EAAE,CAAC,GAAM,MAAM,CAAC,EAGvD,IAAgB,QAAc;EAClC,IAAM,IAAwC,EAAE;AAIhD,SAHA,EAAQ,SAAS,MAAQ;AACvB,KAAQ,EAAI,MAAM,EAAM,QAAQ,MAAM,EAAE,WAAW,EAAI,GAAG;IAC1D,EACK;IACN,CAAC,GAAS,EAAM,CAAC;AAyBpB,QAvBK,EAAQ,SAwBX,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAQ,KAAK,MAAW;GACvB,IAAM,IAAc,EAAc,EAAO,OAAO,EAAE;AAElD,UACE,kBAAC,OAAD;IAAqB,WAAU;cAA/B,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,iBAAiB,EAAO,SAAS,2BAA2B;OACrE,CAAA;MACF,kBAAC,QAAD;OAAM,WAAU;iBAAkD,EAAO;OAAa,CAAA;MACtF,kBAAC,QAAD;OAAM,WAAU;iBAAsC,EAAY;OAAc,CAAA;MAC5E;QAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAY,KAAK,MAChB,kBAAC,OAAD;MAEE,WAAW;;wBAEL,IAAU,kDAAkD,GAAG;;;MAGrE,eAAe,IAAU,EAAK;gBAPhC,CASE,kBAAC,KAAD;OAAG,WAAU;iBAAsD,EAAK;OAAU,CAAA,EAElF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAW,qCAAqC,EAAiB,EAAK,SAAS;kBAClF,EAAK;QACD,CAAA,EAER,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAK;QACD,CAAA,CAEL;SACF;QAtBC,EAAK,GAsBN,CACN,EAED,EAAY,WAAW,KAAK,kBAAC,KAAD;MAAG,WAAU;gBAAkD;MAAY,CAAA,CACpG;OACF;MA1CI,EAAO,GA0CX;IAER;EACE,CAAA,GAxEJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,QAAO;IACP,SAAQ;cAER,kBAAC,QAAD;KACE,eAAc;KACd,gBAAe;KACf,aAAa;KACb,GAAE;KACF,CAAA;IACE,CAAA,EACN,kBAAC,KAAD,EAAA,UAAG,iBAAiB,CAAA,CAChB;;EACF,CAAA;EAyDV"}
@@ -36,9 +36,9 @@ t(function({ widget: t, data: s, onClick: c }) {
36
36
  children: s.metrics.map((t, n) => /* @__PURE__ */ i("div", {
37
37
  className: `
38
38
  flex flex-col p-4
39
- bg-bg-muted/50
39
+ bg-bg-sunken/50
40
40
  rounded-lg
41
- ${d ? "cursor-pointer hover:bg-bg-muted transition-colors" : ""}
41
+ ${d ? "cursor-pointer hover:bg-bg-sunken transition-colors" : ""}
42
42
  `,
43
43
  onClick: () => u(n),
44
44
  role: d ? "button" : void 0,
@@ -1 +1 @@
1
- {"version":3,"file":"KPIWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi/KPIWidget.tsx"],"sourcesContent":["/**\n * KPIWidget Component\n *\n * Multi-KPI comparison card showing multiple metrics together.\n */\n\nimport { type FC, memo, useCallback } from 'react';\nimport type { Widget, KPIConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from '../metric-card/TrendIndicator';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIWidgetProps {\n widget: Widget;\n data?: KPIData;\n onClick?: (metricIndex: number) => void;\n}\n\nexport interface KPIMetric {\n label: string;\n value: number | string;\n previousValue?: number;\n changePercent?: number;\n trend?: TrendDirection;\n unit?: string;\n}\n\nexport interface KPIData {\n metrics: KPIMetric[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number | string, format?: string): string {\n if (typeof value === 'string') return value;\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'percentage':\n return `${(value * 100).toFixed(1)}%`;\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n default:\n return new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n}\n\nfunction getTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIWidget: FC<KPIWidgetProps> = memo(function KPIWidget({ widget, data, onClick }) {\n const config = widget.config as unknown as KPIConfig | undefined;\n\n const handleMetricClick = useCallback(\n (index: number) => {\n if (onClick && widget.drilldowns?.length) {\n onClick(index);\n }\n },\n [onClick, widget.drilldowns]\n );\n\n const isClickable = Boolean(onClick && widget.drilldowns?.length);\n\n if (!data?.metrics?.length) {\n return null;\n }\n\n const layout = config?.layout || 'horizontal';\n\n return (\n <div className=\"flex flex-col h-full p-4\">\n <div\n className={`\n grid gap-4\n ${layout === 'horizontal' ? 'md:grid-cols-2 lg:grid-cols-4' : 'grid-cols-1'}\n flex-1\n `}\n >\n {data.metrics.map((metric, index) => (\n <div\n key={index}\n className={`\n flex flex-col p-4\n bg-bg-muted/50\n rounded-lg\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted transition-colors' : ''}\n `}\n onClick={() => handleMetricClick(index)}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary uppercase tracking-wide mb-1\">{metric.label}</p>\n\n {/* Value */}\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold text-text-primary\">{formatValue(metric.value, config?.format)}</span>\n {metric.unit && <span className=\"text-sm text-text-secondary\">{metric.unit}</span>}\n </div>\n\n {/* Change indicator */}\n {metric.changePercent !== undefined && (\n <div className=\"flex items-center gap-1 mt-2\">\n <TrendIndicator\n direction={metric.trend || getTrend(metric.changePercent)}\n size=\"sm\"\n positiveIsGood={config?.positiveIsGood !== false}\n />\n <span\n className={`text-xs ${\n metric.changePercent >= 0\n ? config?.positiveIsGood !== false\n ? 'text-state-success'\n : 'text-state-error'\n : config?.positiveIsGood !== false\n ? 'text-state-error'\n : 'text-state-success'\n }`}\n >\n {metric.changePercent > 0 ? '+' : ''}\n {metric.changePercent.toFixed(1)}%\n </span>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIWidget;\n"],"mappings":";;;;AAqCA,SAAS,EAAY,GAAwB,GAAyB;AACpE,KAAI,OAAO,KAAU,SAAU,QAAO;AAEtC,SAAQ,GAAR;EACE,KAAK,WACH,QAAO,IAAI,KAAK,aAAa,SAAS;GACpC,OAAO;GACP,UAAU;GACV,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;EAClB,KAAK,aACH,QAAO,IAAI,IAAQ,KAAK,QAAQ,EAAE,CAAC;EACrC,KAAK,UACH,QAAO,IAAI,KAAK,aAAa,SAAS;GACpC,UAAU;GACV,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;EAClB,QACE,QAAO,IAAI,KAAK,aAAa,SAAS,EACpC,uBAAuB,GACxB,CAAC,CAAC,OAAO,EAAM;;;AAItB,SAAS,EAAS,GAAwC;AAExD,QADI,MAAkB,KAAA,KAAa,MAAkB,IAAU,SACxD,IAAgB,IAAI,OAAO;;AAOS,EAAK,SAAmB,EAAE,WAAQ,SAAM,cAAW;CAC9F,IAAM,IAAS,EAAO,QAEhB,IAAoB,GACvB,MAAkB;AACjB,EAAI,KAAW,EAAO,YAAY,UAChC,EAAQ,EAAM;IAGlB,CAAC,GAAS,EAAO,WAAW,CAC7B,EAEK,IAAc,GAAQ,KAAW,EAAO,YAAY;AAQ1D,QANK,GAAM,SAAS,SAOlB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GACE,WAAW;;aALF,GAAQ,UAAU,kBAOZ,eAAe,kCAAkC,cAAc;;;aAI7E,EAAK,QAAQ,KAAK,GAAQ,MACzB,kBAAC,OAAD;IAEE,WAAW;;;;gBAIP,IAAc,uDAAuD,GAAG;;IAE5E,eAAe,EAAkB,EAAM;IACvC,MAAM,IAAc,WAAW,KAAA;IAC/B,UAAU,IAAc,IAAI,KAAA;cAV9B;KAaE,kBAAC,KAAD;MAAG,WAAU;gBAA4D,EAAO;MAAU,CAAA;KAG1F,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAwC,EAAY,EAAO,OAAO,GAAQ,OAAO;OAAQ,CAAA,EACxG,EAAO,QAAQ,kBAAC,QAAD;OAAM,WAAU;iBAA+B,EAAO;OAAY,CAAA,CAC9E;;KAGL,EAAO,kBAAkB,KAAA,KACxB,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,WAAW,EAAO,SAAS,EAAS,EAAO,cAAc;OACzD,MAAK;OACL,gBAAgB,GAAQ,mBAAmB;OAC3C,CAAA,EACF,kBAAC,QAAD;OACE,WAAW,WACT,EAAO,iBAAiB,IACpB,GAAQ,mBAAmB,KAEzB,qBADA,uBAEF,GAAQ,mBAAmB,KAEzB,uBADA;iBAPV;QAWG,EAAO,gBAAgB,IAAI,MAAM;QACjC,EAAO,cAAc,QAAQ,EAAE;QAAC;QAC5B;SACH;;KAEJ;MA5CC,EA4CD,CACN;GACE,CAAA;EACF,CAAA,GA/DC;EAiET"}
1
+ {"version":3,"file":"KPIWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi/KPIWidget.tsx"],"sourcesContent":["/**\n * KPIWidget Component\n *\n * Multi-KPI comparison card showing multiple metrics together.\n */\n\nimport { type FC, memo, useCallback } from 'react';\nimport type { Widget, KPIConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from '../metric-card/TrendIndicator';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIWidgetProps {\n widget: Widget;\n data?: KPIData;\n onClick?: (metricIndex: number) => void;\n}\n\nexport interface KPIMetric {\n label: string;\n value: number | string;\n previousValue?: number;\n changePercent?: number;\n trend?: TrendDirection;\n unit?: string;\n}\n\nexport interface KPIData {\n metrics: KPIMetric[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number | string, format?: string): string {\n if (typeof value === 'string') return value;\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'percentage':\n return `${(value * 100).toFixed(1)}%`;\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n default:\n return new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n}\n\nfunction getTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIWidget: FC<KPIWidgetProps> = memo(function KPIWidget({ widget, data, onClick }) {\n const config = widget.config as unknown as KPIConfig | undefined;\n\n const handleMetricClick = useCallback(\n (index: number) => {\n if (onClick && widget.drilldowns?.length) {\n onClick(index);\n }\n },\n [onClick, widget.drilldowns]\n );\n\n const isClickable = Boolean(onClick && widget.drilldowns?.length);\n\n if (!data?.metrics?.length) {\n return null;\n }\n\n const layout = config?.layout || 'horizontal';\n\n return (\n <div className=\"flex flex-col h-full p-4\">\n <div\n className={`\n grid gap-4\n ${layout === 'horizontal' ? 'md:grid-cols-2 lg:grid-cols-4' : 'grid-cols-1'}\n flex-1\n `}\n >\n {data.metrics.map((metric, index) => (\n <div\n key={index}\n className={`\n flex flex-col p-4\n bg-bg-sunken/50\n rounded-lg\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken transition-colors' : ''}\n `}\n onClick={() => handleMetricClick(index)}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary uppercase tracking-wide mb-1\">{metric.label}</p>\n\n {/* Value */}\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold text-text-primary\">{formatValue(metric.value, config?.format)}</span>\n {metric.unit && <span className=\"text-sm text-text-secondary\">{metric.unit}</span>}\n </div>\n\n {/* Change indicator */}\n {metric.changePercent !== undefined && (\n <div className=\"flex items-center gap-1 mt-2\">\n <TrendIndicator\n direction={metric.trend || getTrend(metric.changePercent)}\n size=\"sm\"\n positiveIsGood={config?.positiveIsGood !== false}\n />\n <span\n className={`text-xs ${\n metric.changePercent >= 0\n ? config?.positiveIsGood !== false\n ? 'text-state-success'\n : 'text-state-error'\n : config?.positiveIsGood !== false\n ? 'text-state-error'\n : 'text-state-success'\n }`}\n >\n {metric.changePercent > 0 ? '+' : ''}\n {metric.changePercent.toFixed(1)}%\n </span>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIWidget;\n"],"mappings":";;;;AAqCA,SAAS,EAAY,GAAwB,GAAyB;AACpE,KAAI,OAAO,KAAU,SAAU,QAAO;AAEtC,SAAQ,GAAR;EACE,KAAK,WACH,QAAO,IAAI,KAAK,aAAa,SAAS;GACpC,OAAO;GACP,UAAU;GACV,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;EAClB,KAAK,aACH,QAAO,IAAI,IAAQ,KAAK,QAAQ,EAAE,CAAC;EACrC,KAAK,UACH,QAAO,IAAI,KAAK,aAAa,SAAS;GACpC,UAAU;GACV,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;EAClB,QACE,QAAO,IAAI,KAAK,aAAa,SAAS,EACpC,uBAAuB,GACxB,CAAC,CAAC,OAAO,EAAM;;;AAItB,SAAS,EAAS,GAAwC;AAExD,QADI,MAAkB,KAAA,KAAa,MAAkB,IAAU,SACxD,IAAgB,IAAI,OAAO;;AAOS,EAAK,SAAmB,EAAE,WAAQ,SAAM,cAAW;CAC9F,IAAM,IAAS,EAAO,QAEhB,IAAoB,GACvB,MAAkB;AACjB,EAAI,KAAW,EAAO,YAAY,UAChC,EAAQ,EAAM;IAGlB,CAAC,GAAS,EAAO,WAAW,CAC7B,EAEK,IAAc,GAAQ,KAAW,EAAO,YAAY;AAQ1D,QANK,GAAM,SAAS,SAOlB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GACE,WAAW;;aALF,GAAQ,UAAU,kBAOZ,eAAe,kCAAkC,cAAc;;;aAI7E,EAAK,QAAQ,KAAK,GAAQ,MACzB,kBAAC,OAAD;IAEE,WAAW;;;;gBAIP,IAAc,wDAAwD,GAAG;;IAE7E,eAAe,EAAkB,EAAM;IACvC,MAAM,IAAc,WAAW,KAAA;IAC/B,UAAU,IAAc,IAAI,KAAA;cAV9B;KAaE,kBAAC,KAAD;MAAG,WAAU;gBAA4D,EAAO;MAAU,CAAA;KAG1F,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAwC,EAAY,EAAO,OAAO,GAAQ,OAAO;OAAQ,CAAA,EACxG,EAAO,QAAQ,kBAAC,QAAD;OAAM,WAAU;iBAA+B,EAAO;OAAY,CAAA,CAC9E;;KAGL,EAAO,kBAAkB,KAAA,KACxB,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,WAAW,EAAO,SAAS,EAAS,EAAO,cAAc;OACzD,MAAK;OACL,gBAAgB,GAAQ,mBAAmB;OAC3C,CAAA,EACF,kBAAC,QAAD;OACE,WAAW,WACT,EAAO,iBAAiB,IACpB,GAAQ,mBAAmB,KAEzB,qBADA,uBAEF,GAAQ,mBAAmB,KAEzB,uBADA;iBAPV;QAWG,EAAO,gBAAgB,IAAI,MAAM;QACjC,EAAO,cAAc,QAAQ,EAAE;QAAC;QAC5B;SACH;;KAEJ;MA5CC,EA4CD,CACN;GACE,CAAA;EACF,CAAA,GA/DC;EAiET"}
@@ -116,7 +116,7 @@ var c = n(function({ widget: n, data: c, onClick: l, onDrilldown: u }) {
116
116
  className: "grid grid-cols-2 gap-3",
117
117
  children: m.map((n) => /* @__PURE__ */ i("div", {
118
118
  className: `
119
- p-3 rounded-lg border border-border-default bg-bg-muted/30
119
+ p-3 rounded-lg border border-border-default bg-bg-sunken/30
120
120
  ${l || d ? "cursor-pointer hover:border-action-primary-bg transition-colors" : ""}
121
121
  `,
122
122
  onClick: () => {
@@ -157,7 +157,7 @@ var c = n(function({ widget: n, data: c, onClick: l, onDrilldown: u }) {
157
157
  className: "flex justify-between text-[10px] text-text-tertiary mb-0.5",
158
158
  children: [/* @__PURE__ */ r("span", { children: "vs target" }), /* @__PURE__ */ i("span", { children: [(n.value / n.target * 100).toFixed(0), "%"] })]
159
159
  }), /* @__PURE__ */ r("div", {
160
- className: "h-1 bg-bg-muted rounded-full overflow-hidden",
160
+ className: "h-1 bg-bg-sunken rounded-full overflow-hidden",
161
161
  children: /* @__PURE__ */ r("div", {
162
162
  className: `h-full rounded-full ${n.value >= n.target ? "bg-state-success" : "bg-action-primary-bg"}`,
163
163
  style: { width: `${Math.min(n.value / n.target * 100, 100)}%` }
@@ -1 +1 @@
1
- {"version":3,"file":"KPIComparisonWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx"],"sourcesContent":["/**\n * KPIComparisonWidget Component\n *\n * Displays multiple KPIs in a grid for comparison.\n */\n\nimport { type FC, memo } from 'react';\nimport type { Widget } from '../../../types';\nimport { type ConditionalRule, resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIComparisonWidgetProps {\n widget: Widget;\n data?: KPIComparisonData;\n onClick?: (kpi: KPIItem) => void;\n onDrilldown?: (data: Record<string, unknown>) => void;\n}\n\nexport interface KPIItem {\n id: string;\n label: string;\n value: number;\n previousValue?: number;\n target?: number;\n unit?: string;\n trend?: 'up' | 'down' | 'stable';\n trendValue?: number;\n}\n\nexport interface KPIComparisonData {\n kpis?: KPIItem[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number, unit?: string): string {\n if (unit === '$') {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n }\n if (unit === '%') {\n return `${value.toFixed(1)}%`;\n }\n return new Intl.NumberFormat('en-US', {\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction getTrendColor(trend?: string, trendValue?: number): string {\n if (!trend || trend === 'stable') return 'text-text-secondary';\n if (trend === 'up' && (trendValue ?? 0) >= 0) return 'text-state-success';\n if (trend === 'down' && (trendValue ?? 0) < 0) return 'text-state-error';\n return 'text-text-secondary';\n}\n\nfunction getTrendIcon(trend?: string): string {\n if (trend === 'up') return '↑';\n if (trend === 'down') return '↓';\n return '→';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIComparisonWidget: FC<KPIComparisonWidgetProps> = memo(function KPIComparisonWidget({\n widget,\n data,\n onClick,\n onDrilldown,\n}) {\n const hasDrilldown = !!widget.drilldown?.enabled;\n const config = widget.config as Record<string, unknown> | undefined;\n const conditionalRules = config?.conditionalRules as ConditionalRule[] | undefined;\n\n // Build KPIs array from data\n // Support multiple formats:\n // 1. data.kpis array (standard format)\n // 2. Single value from data.processed (e.g., 125000 from DataSink)\n // 3. data is a number/string directly\n // 4. Nested summary object (e.g., {sales: [...], summary: {totalSales: 123}})\n // 5. Array of objects from DataSink (e.g., [{month: \"Jan\", revenue: 45000}, ...])\n let kpis: KPIItem[] = [];\n\n // Get configured value field from widget config (for DataSink data)\n const valueField = (config?.valueField as string) || (config?.value_field as string) || 'value';\n const labelField = (config?.labelField as string) || (config?.label_field as string) || 'label';\n\n // Helper to convert object with numeric values to KPIs\n const objectToKpis = (obj: Record<string, unknown>): KPIItem[] => {\n const entries = Object.entries(obj);\n return entries\n .filter(([, v]) => typeof v === 'number')\n .map(([key, value]) => ({\n id: key,\n label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: value as number,\n unit:\n key.toLowerCase().includes('sales') ||\n key.toLowerCase().includes('revenue') ||\n key.toLowerCase().includes('value')\n ? '$'\n : undefined,\n }));\n };\n\n // Helper to extract KPI from a single data row using config fields\n const extractKpiFromRow = (row: Record<string, unknown>, index: number): KPIItem | null => {\n // Try to get value from configured field or common field names\n const valueCandidates = [valueField, 'value', 'revenue', 'amount', 'total', 'count', 'sales', 'growth'];\n let value: number | null = null;\n\n for (const field of valueCandidates) {\n if (typeof row[field] === 'number') {\n value = row[field] as number;\n break;\n }\n if (typeof row[field] === 'string' && !isNaN(parseFloat(row[field] as string))) {\n value = parseFloat(row[field] as string);\n break;\n }\n }\n\n if (value === null) {\n // Try to find any numeric field\n const numericEntry = Object.entries(row).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n value = numericEntry[1] as number;\n }\n }\n\n if (value === null) return null;\n\n // Try to get label from configured field or common field names\n const labelCandidates = [labelField, 'label', 'name', 'title', 'month', 'region', 'category'];\n let label = `Item ${index + 1}`;\n\n for (const field of labelCandidates) {\n if (row[field] && typeof row[field] === 'string') {\n label = row[field] as string;\n break;\n }\n }\n\n // Check for growth/trend fields\n const growthValue = row.growth ?? row.change ?? row.trend ?? row.percentChange;\n const trend =\n typeof growthValue === 'number' ? (growthValue > 0 ? 'up' : growthValue < 0 ? 'down' : 'stable') : undefined;\n\n return {\n id: `kpi-${index}`,\n label,\n value,\n unit:\n valueField.toLowerCase().includes('revenue') || valueField.toLowerCase().includes('sales') ? '$' : undefined,\n trend: trend as 'up' | 'down' | 'stable' | undefined,\n trendValue: typeof growthValue === 'number' ? growthValue : undefined,\n };\n };\n\n if (data?.kpis && Array.isArray(data.kpis)) {\n // Standard KPIs array\n kpis = data.kpis;\n } else if (Array.isArray(data) && data.length > 0) {\n // NEW: Handle array of objects from DataSink\n // Extract KPIs from each row, up to 4 items for display\n const maxKpis = 4;\n for (let i = 0; i < Math.min(data.length, maxKpis); i++) {\n const row = data[i] as Record<string, unknown>;\n if (row && typeof row === 'object') {\n const kpi = extractKpiFromRow(row, i);\n if (kpi) kpis.push(kpi);\n }\n }\n\n // If we couldn't extract KPIs from rows, try to aggregate numeric fields\n if (kpis.length === 0 && data.length > 0) {\n const firstRow = data[0] as Record<string, unknown>;\n const numericFields = Object.entries(firstRow).filter(([, v]) => typeof v === 'number');\n\n // Create KPIs by aggregating (summing) each numeric field across all rows\n kpis = numericFields.slice(0, maxKpis).map(([field]) => {\n const sum = data.reduce((acc, row) => {\n const val = (row as Record<string, unknown>)[field];\n return acc + (typeof val === 'number' ? val : 0);\n }, 0);\n\n return {\n id: field,\n label: field.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: sum,\n unit: field.toLowerCase().includes('revenue') || field.toLowerCase().includes('sales') ? '$' : undefined,\n };\n });\n }\n } else if (typeof data === 'number' || typeof data === 'string') {\n // Single value - create a KPI from it using widget config\n const value = typeof data === 'string' ? parseFloat(data) : data;\n if (!isNaN(value)) {\n kpis = [\n {\n id: 'primary',\n label: ((config?.subtitle as string) || widget.title || 'Value').trim(),\n value,\n unit: config?.format === 'currency' ? '$' : config?.format === 'percent' ? '%' : undefined,\n },\n ];\n }\n } else if (data && typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as Record<string, unknown>;\n\n // First, check if there's a nested 'summary' object with numeric values\n if ('summary' in dataObj && dataObj.summary && typeof dataObj.summary === 'object') {\n kpis = objectToKpis(dataObj.summary as Record<string, unknown>);\n }\n\n // If no KPIs from summary, try top-level numeric values\n if (kpis.length === 0) {\n kpis = objectToKpis(dataObj);\n }\n }\n\n if (!kpis.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No KPI data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full p-4 overflow-auto\">\n <div className=\"grid grid-cols-2 gap-3\">\n {kpis.map((kpi) => (\n <div\n key={kpi.id}\n className={`\n p-3 rounded-lg border border-border-default bg-bg-muted/30\n ${onClick || hasDrilldown ? 'cursor-pointer hover:border-action-primary-bg transition-colors' : ''}\n `}\n onClick={() => {\n // Trigger drilldown with KPI data if enabled\n if (hasDrilldown && onDrilldown) {\n onDrilldown({\n kpiId: kpi.id,\n kpiLabel: kpi.label,\n kpiValue: kpi.value,\n kpiUnit: kpi.unit,\n kpiTrend: kpi.trend,\n kpiTrendValue: kpi.trendValue,\n kpiTarget: kpi.target,\n kpiPreviousValue: kpi.previousValue,\n });\n }\n onClick?.(kpi);\n }}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary truncate mb-1\">{kpi.label}</p>\n\n {/* Value (with optional threshold-based conditional color) */}\n <p\n className={`text-xl font-semibold ${\n conditionalStyleToClassName(\n resolveConditionalStyle(kpi.value, conditionalRules, { [kpi.label]: kpi.value, value: kpi.value })\n ) || 'text-text-primary'\n }`}\n >\n {formatValue(kpi.value, kpi.unit)}\n </p>\n\n {/* Trend */}\n {kpi.trendValue !== undefined && (\n <div className={`flex items-center gap-1 mt-1 text-xs ${getTrendColor(kpi.trend, kpi.trendValue)}`}>\n <span>{getTrendIcon(kpi.trend)}</span>\n <span>\n {kpi.trendValue >= 0 ? '+' : ''}\n {kpi.trendValue.toFixed(1)}%\n </span>\n </div>\n )}\n\n {/* Target progress */}\n {kpi.target !== undefined && (\n <div className=\"mt-2\">\n <div className=\"flex justify-between text-[10px] text-text-tertiary mb-0.5\">\n <span>vs target</span>\n <span>{((kpi.value / kpi.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1 bg-bg-muted rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${kpi.value >= kpi.target ? 'bg-state-success' : 'bg-action-primary-bg'}`}\n style={{ width: `${Math.min((kpi.value / kpi.target) * 100, 100)}%` }}\n />\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIComparisonWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"KPIComparisonWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx"],"sourcesContent":["/**\n * KPIComparisonWidget Component\n *\n * Displays multiple KPIs in a grid for comparison.\n */\n\nimport { type FC, memo } from 'react';\nimport type { Widget } from '../../../types';\nimport { type ConditionalRule, resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIComparisonWidgetProps {\n widget: Widget;\n data?: KPIComparisonData;\n onClick?: (kpi: KPIItem) => void;\n onDrilldown?: (data: Record<string, unknown>) => void;\n}\n\nexport interface KPIItem {\n id: string;\n label: string;\n value: number;\n previousValue?: number;\n target?: number;\n unit?: string;\n trend?: 'up' | 'down' | 'stable';\n trendValue?: number;\n}\n\nexport interface KPIComparisonData {\n kpis?: KPIItem[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number, unit?: string): string {\n if (unit === '$') {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n }\n if (unit === '%') {\n return `${value.toFixed(1)}%`;\n }\n return new Intl.NumberFormat('en-US', {\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction getTrendColor(trend?: string, trendValue?: number): string {\n if (!trend || trend === 'stable') return 'text-text-secondary';\n if (trend === 'up' && (trendValue ?? 0) >= 0) return 'text-state-success';\n if (trend === 'down' && (trendValue ?? 0) < 0) return 'text-state-error';\n return 'text-text-secondary';\n}\n\nfunction getTrendIcon(trend?: string): string {\n if (trend === 'up') return '↑';\n if (trend === 'down') return '↓';\n return '→';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIComparisonWidget: FC<KPIComparisonWidgetProps> = memo(function KPIComparisonWidget({\n widget,\n data,\n onClick,\n onDrilldown,\n}) {\n const hasDrilldown = !!widget.drilldown?.enabled;\n const config = widget.config as Record<string, unknown> | undefined;\n const conditionalRules = config?.conditionalRules as ConditionalRule[] | undefined;\n\n // Build KPIs array from data\n // Support multiple formats:\n // 1. data.kpis array (standard format)\n // 2. Single value from data.processed (e.g., 125000 from DataSink)\n // 3. data is a number/string directly\n // 4. Nested summary object (e.g., {sales: [...], summary: {totalSales: 123}})\n // 5. Array of objects from DataSink (e.g., [{month: \"Jan\", revenue: 45000}, ...])\n let kpis: KPIItem[] = [];\n\n // Get configured value field from widget config (for DataSink data)\n const valueField = (config?.valueField as string) || (config?.value_field as string) || 'value';\n const labelField = (config?.labelField as string) || (config?.label_field as string) || 'label';\n\n // Helper to convert object with numeric values to KPIs\n const objectToKpis = (obj: Record<string, unknown>): KPIItem[] => {\n const entries = Object.entries(obj);\n return entries\n .filter(([, v]) => typeof v === 'number')\n .map(([key, value]) => ({\n id: key,\n label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: value as number,\n unit:\n key.toLowerCase().includes('sales') ||\n key.toLowerCase().includes('revenue') ||\n key.toLowerCase().includes('value')\n ? '$'\n : undefined,\n }));\n };\n\n // Helper to extract KPI from a single data row using config fields\n const extractKpiFromRow = (row: Record<string, unknown>, index: number): KPIItem | null => {\n // Try to get value from configured field or common field names\n const valueCandidates = [valueField, 'value', 'revenue', 'amount', 'total', 'count', 'sales', 'growth'];\n let value: number | null = null;\n\n for (const field of valueCandidates) {\n if (typeof row[field] === 'number') {\n value = row[field] as number;\n break;\n }\n if (typeof row[field] === 'string' && !isNaN(parseFloat(row[field] as string))) {\n value = parseFloat(row[field] as string);\n break;\n }\n }\n\n if (value === null) {\n // Try to find any numeric field\n const numericEntry = Object.entries(row).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n value = numericEntry[1] as number;\n }\n }\n\n if (value === null) return null;\n\n // Try to get label from configured field or common field names\n const labelCandidates = [labelField, 'label', 'name', 'title', 'month', 'region', 'category'];\n let label = `Item ${index + 1}`;\n\n for (const field of labelCandidates) {\n if (row[field] && typeof row[field] === 'string') {\n label = row[field] as string;\n break;\n }\n }\n\n // Check for growth/trend fields\n const growthValue = row.growth ?? row.change ?? row.trend ?? row.percentChange;\n const trend =\n typeof growthValue === 'number' ? (growthValue > 0 ? 'up' : growthValue < 0 ? 'down' : 'stable') : undefined;\n\n return {\n id: `kpi-${index}`,\n label,\n value,\n unit:\n valueField.toLowerCase().includes('revenue') || valueField.toLowerCase().includes('sales') ? '$' : undefined,\n trend: trend as 'up' | 'down' | 'stable' | undefined,\n trendValue: typeof growthValue === 'number' ? growthValue : undefined,\n };\n };\n\n if (data?.kpis && Array.isArray(data.kpis)) {\n // Standard KPIs array\n kpis = data.kpis;\n } else if (Array.isArray(data) && data.length > 0) {\n // NEW: Handle array of objects from DataSink\n // Extract KPIs from each row, up to 4 items for display\n const maxKpis = 4;\n for (let i = 0; i < Math.min(data.length, maxKpis); i++) {\n const row = data[i] as Record<string, unknown>;\n if (row && typeof row === 'object') {\n const kpi = extractKpiFromRow(row, i);\n if (kpi) kpis.push(kpi);\n }\n }\n\n // If we couldn't extract KPIs from rows, try to aggregate numeric fields\n if (kpis.length === 0 && data.length > 0) {\n const firstRow = data[0] as Record<string, unknown>;\n const numericFields = Object.entries(firstRow).filter(([, v]) => typeof v === 'number');\n\n // Create KPIs by aggregating (summing) each numeric field across all rows\n kpis = numericFields.slice(0, maxKpis).map(([field]) => {\n const sum = data.reduce((acc, row) => {\n const val = (row as Record<string, unknown>)[field];\n return acc + (typeof val === 'number' ? val : 0);\n }, 0);\n\n return {\n id: field,\n label: field.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: sum,\n unit: field.toLowerCase().includes('revenue') || field.toLowerCase().includes('sales') ? '$' : undefined,\n };\n });\n }\n } else if (typeof data === 'number' || typeof data === 'string') {\n // Single value - create a KPI from it using widget config\n const value = typeof data === 'string' ? parseFloat(data) : data;\n if (!isNaN(value)) {\n kpis = [\n {\n id: 'primary',\n label: ((config?.subtitle as string) || widget.title || 'Value').trim(),\n value,\n unit: config?.format === 'currency' ? '$' : config?.format === 'percent' ? '%' : undefined,\n },\n ];\n }\n } else if (data && typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as Record<string, unknown>;\n\n // First, check if there's a nested 'summary' object with numeric values\n if ('summary' in dataObj && dataObj.summary && typeof dataObj.summary === 'object') {\n kpis = objectToKpis(dataObj.summary as Record<string, unknown>);\n }\n\n // If no KPIs from summary, try top-level numeric values\n if (kpis.length === 0) {\n kpis = objectToKpis(dataObj);\n }\n }\n\n if (!kpis.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No KPI data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full p-4 overflow-auto\">\n <div className=\"grid grid-cols-2 gap-3\">\n {kpis.map((kpi) => (\n <div\n key={kpi.id}\n className={`\n p-3 rounded-lg border border-border-default bg-bg-sunken/30\n ${onClick || hasDrilldown ? 'cursor-pointer hover:border-action-primary-bg transition-colors' : ''}\n `}\n onClick={() => {\n // Trigger drilldown with KPI data if enabled\n if (hasDrilldown && onDrilldown) {\n onDrilldown({\n kpiId: kpi.id,\n kpiLabel: kpi.label,\n kpiValue: kpi.value,\n kpiUnit: kpi.unit,\n kpiTrend: kpi.trend,\n kpiTrendValue: kpi.trendValue,\n kpiTarget: kpi.target,\n kpiPreviousValue: kpi.previousValue,\n });\n }\n onClick?.(kpi);\n }}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary truncate mb-1\">{kpi.label}</p>\n\n {/* Value (with optional threshold-based conditional color) */}\n <p\n className={`text-xl font-semibold ${\n conditionalStyleToClassName(\n resolveConditionalStyle(kpi.value, conditionalRules, { [kpi.label]: kpi.value, value: kpi.value })\n ) || 'text-text-primary'\n }`}\n >\n {formatValue(kpi.value, kpi.unit)}\n </p>\n\n {/* Trend */}\n {kpi.trendValue !== undefined && (\n <div className={`flex items-center gap-1 mt-1 text-xs ${getTrendColor(kpi.trend, kpi.trendValue)}`}>\n <span>{getTrendIcon(kpi.trend)}</span>\n <span>\n {kpi.trendValue >= 0 ? '+' : ''}\n {kpi.trendValue.toFixed(1)}%\n </span>\n </div>\n )}\n\n {/* Target progress */}\n {kpi.target !== undefined && (\n <div className=\"mt-2\">\n <div className=\"flex justify-between text-[10px] text-text-tertiary mb-0.5\">\n <span>vs target</span>\n <span>{((kpi.value / kpi.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${kpi.value >= kpi.target ? 'bg-state-success' : 'bg-action-primary-bg'}`}\n style={{ width: `${Math.min((kpi.value / kpi.target) * 100, 100)}%` }}\n />\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIComparisonWidget;\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ function r(e) {
7
7
  case "warning": return "bg-state-warning/10 text-state-warning";
8
8
  case "error": return "bg-state-error/10 text-state-error";
9
9
  case "info": return "bg-action-primary-bg/10 text-action-primary-bg";
10
- default: return "bg-bg-muted text-text-secondary";
10
+ default: return "bg-bg-sunken text-text-secondary";
11
11
  }
12
12
  }
13
13
  var i = e(function({ item: e, clickable: i = !1, onClick: a, showDivider: o = !0 }) {
@@ -16,7 +16,7 @@ var i = e(function({ item: e, clickable: i = !1, onClick: a, showDivider: o = !0
16
16
  className: `
17
17
  flex items-start gap-3 p-3
18
18
  ${o ? "border-b border-border-default" : ""}
19
- ${s ? "cursor-pointer hover:bg-bg-muted/50 transition-colors" : ""}
19
+ ${s ? "cursor-pointer hover:bg-bg-sunken/50 transition-colors" : ""}
20
20
  ${e.disabled ? "opacity-50" : ""}
21
21
  `,
22
22
  onClick: () => {
@@ -29,7 +29,7 @@ var i = e(function({ item: e, clickable: i = !1, onClick: a, showDivider: o = !0
29
29
  },
30
30
  children: [
31
31
  e.icon && /* @__PURE__ */ t("div", {
32
- className: "flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full bg-bg-muted text-text-secondary",
32
+ className: "flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full bg-bg-sunken text-text-secondary",
33
33
  children: e.icon
34
34
  }),
35
35
  /* @__PURE__ */ n("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"ListItem.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/list/ListItem.tsx"],"sourcesContent":["/**\n * ListItem Component\n *\n * Individual item in a list widget.\n */\n\nimport { type FC, memo, type ReactNode } from 'react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ListItemData {\n /** Unique identifier */\n id: string;\n /** Primary text */\n title: string;\n /** Secondary text */\n subtitle?: string;\n /** Additional metadata lines */\n metadata?: Array<{\n label: string;\n value: string | number;\n }>;\n /** Badge/tag to display */\n badge?: {\n text: string;\n color?: 'default' | 'success' | 'warning' | 'error' | 'info';\n };\n /** Icon or avatar */\n icon?: ReactNode;\n /** Value to display on the right */\n value?: string | number;\n /** Whether item is disabled */\n disabled?: boolean;\n}\n\nexport interface ListItemProps {\n /** Item data */\n item: ListItemData;\n /** Whether the item is clickable */\n clickable?: boolean;\n /** Click handler */\n onClick?: () => void;\n /** Whether to show divider */\n showDivider?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getBadgeColor(badge: ListItemData['badge']): string {\n const color = badge?.color;\n switch (color) {\n case 'success':\n return 'bg-state-success/10 text-state-success';\n case 'warning':\n return 'bg-state-warning/10 text-state-warning';\n case 'error':\n return 'bg-state-error/10 text-state-error';\n case 'info':\n return 'bg-action-primary-bg/10 text-action-primary-bg';\n case 'default':\n default:\n return 'bg-bg-muted text-text-secondary';\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const ListItem: FC<ListItemProps> = memo(function ListItem({\n item,\n clickable = false,\n onClick,\n showDivider = true,\n}) {\n const isClickable = clickable && !item.disabled;\n\n return (\n <div\n className={`\n flex items-start gap-3 p-3\n ${showDivider ? 'border-b border-border-default' : ''}\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50 transition-colors' : ''}\n ${item.disabled ? 'opacity-50' : ''}\n `}\n onClick={() => {\n if (isClickable && onClick) {\n onClick();\n }\n }}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && onClick && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n onClick();\n }\n }}\n >\n {/* Icon/Avatar */}\n {item.icon && (\n <div className=\"flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full bg-bg-muted text-text-secondary\">\n {item.icon}\n </div>\n )}\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Title row */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"text-sm font-medium text-text-primary truncate\">{item.title}</h4>\n {item.subtitle && <p className=\"text-xs text-text-secondary mt-0.5 line-clamp-2\">{item.subtitle}</p>}\n </div>\n\n {/* Value */}\n {item.value !== undefined && (\n <span className=\"flex-shrink-0 text-sm font-medium text-text-primary\">\n {typeof item.value === 'number' ? new Intl.NumberFormat('en-US').format(item.value) : item.value}\n </span>\n )}\n </div>\n\n {/* Metadata */}\n {item.metadata && item.metadata.length > 0 && (\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 mt-2\">\n {item.metadata.map((meta, index) => (\n <span key={index} className=\"text-xs text-text-secondary\">\n <span className=\"text-text-secondary/70\">{meta.label}: </span>\n <span className=\"font-medium\">{meta.value}</span>\n </span>\n ))}\n </div>\n )}\n\n {/* Badge */}\n {item.badge && (\n <div className=\"mt-2\">\n <span\n className={`\n inline-flex items-center\n px-2 py-0.5\n text-xs font-medium\n rounded-full\n ${getBadgeColor(item.badge)}\n `}\n >\n {item.badge.text}\n </span>\n </div>\n )}\n </div>\n\n {/* Chevron for clickable items */}\n {isClickable && (\n <div className=\"flex-shrink-0 self-center\">\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </div>\n )}\n </div>\n );\n});\n\nexport default ListItem;\n"],"mappings":";;;AAoDA,SAAS,EAAc,GAAsC;AAE3D,SADc,GAAO,OACrB;EACE,KAAK,UACH,QAAO;EACT,KAAK,UACH,QAAO;EACT,KAAK,QACH,QAAO;EACT,KAAK,OACH,QAAO;EAET,QACE,QAAO;;;AAQb,IAAa,IAA8B,EAAK,SAAkB,EAChE,SACA,eAAY,IACZ,YACA,iBAAc,MACb;CACD,IAAM,IAAc,KAAa,CAAC,EAAK;AAEvC,QACE,kBAAC,OAAD;EACE,WAAW;;UAEP,IAAc,mCAAmC,GAAG;UACpD,IAAc,0DAA0D,GAAG;UAC3E,EAAK,WAAW,eAAe,GAAG;;EAEtC,eAAe;AACb,GAAI,KAAe,KACjB,GAAS;;EAGb,MAAM,IAAc,WAAW,KAAA;EAC/B,UAAU,IAAc,IAAI,KAAA;EAC5B,YAAY,MAAM;AAChB,GAAI,KAAe,MAAY,EAAE,QAAQ,WAAW,EAAE,QAAQ,SAC5D,EAAE,gBAAgB,EAClB,GAAS;;YAjBf;GAsBG,EAAK,QACJ,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAK;IACF,CAAA;GAIR,kBAAC,OAAD;IAAK,WAAU;cAAf;KAEE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAkD,EAAK;QAAW,CAAA,EAC/E,EAAK,YAAY,kBAAC,KAAD;QAAG,WAAU;kBAAmD,EAAK;QAAa,CAAA,CAChG;UAGL,EAAK,UAAU,KAAA,KACd,kBAAC,QAAD;OAAM,WAAU;iBACb,OAAO,EAAK,SAAU,WAAW,IAAI,KAAK,aAAa,QAAQ,CAAC,OAAO,EAAK,MAAM,GAAG,EAAK;OACtF,CAAA,CAEL;;KAGL,EAAK,YAAY,EAAK,SAAS,SAAS,KACvC,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAK,SAAS,KAAK,GAAM,MACxB,kBAAC,QAAD;OAAkB,WAAU;iBAA5B,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAA0C,EAAK,OAAM,KAAS;WAC9D,kBAAC,QAAD;QAAM,WAAU;kBAAe,EAAK;QAAa,CAAA,CAC5C;SAHI,EAGJ,CACP;MACE,CAAA;KAIP,EAAK,SACJ,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,QAAD;OACE,WAAW;;;;;kBAKP,EAAc,EAAK,MAAM,CAAC;;iBAG7B,EAAK,MAAM;OACP,CAAA;MACH,CAAA;KAEJ;;GAGL,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;KAA8B,MAAK;KAAO,QAAO;KAAe,SAAQ;eACrF,kBAAC,QAAD;MAAM,eAAc;MAAQ,gBAAe;MAAQ,aAAa;MAAG,GAAE;MAAiB,CAAA;KAClF,CAAA;IACF,CAAA;GAEJ;;EAER"}
1
+ {"version":3,"file":"ListItem.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/list/ListItem.tsx"],"sourcesContent":["/**\n * ListItem Component\n *\n * Individual item in a list widget.\n */\n\nimport { type FC, memo, type ReactNode } from 'react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ListItemData {\n /** Unique identifier */\n id: string;\n /** Primary text */\n title: string;\n /** Secondary text */\n subtitle?: string;\n /** Additional metadata lines */\n metadata?: Array<{\n label: string;\n value: string | number;\n }>;\n /** Badge/tag to display */\n badge?: {\n text: string;\n color?: 'default' | 'success' | 'warning' | 'error' | 'info';\n };\n /** Icon or avatar */\n icon?: ReactNode;\n /** Value to display on the right */\n value?: string | number;\n /** Whether item is disabled */\n disabled?: boolean;\n}\n\nexport interface ListItemProps {\n /** Item data */\n item: ListItemData;\n /** Whether the item is clickable */\n clickable?: boolean;\n /** Click handler */\n onClick?: () => void;\n /** Whether to show divider */\n showDivider?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getBadgeColor(badge: ListItemData['badge']): string {\n const color = badge?.color;\n switch (color) {\n case 'success':\n return 'bg-state-success/10 text-state-success';\n case 'warning':\n return 'bg-state-warning/10 text-state-warning';\n case 'error':\n return 'bg-state-error/10 text-state-error';\n case 'info':\n return 'bg-action-primary-bg/10 text-action-primary-bg';\n case 'default':\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const ListItem: FC<ListItemProps> = memo(function ListItem({\n item,\n clickable = false,\n onClick,\n showDivider = true,\n}) {\n const isClickable = clickable && !item.disabled;\n\n return (\n <div\n className={`\n flex items-start gap-3 p-3\n ${showDivider ? 'border-b border-border-default' : ''}\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken/50 transition-colors' : ''}\n ${item.disabled ? 'opacity-50' : ''}\n `}\n onClick={() => {\n if (isClickable && onClick) {\n onClick();\n }\n }}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && onClick && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n onClick();\n }\n }}\n >\n {/* Icon/Avatar */}\n {item.icon && (\n <div className=\"flex-shrink-0 w-10 h-10 flex items-center justify-center rounded-full bg-bg-sunken text-text-secondary\">\n {item.icon}\n </div>\n )}\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Title row */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"text-sm font-medium text-text-primary truncate\">{item.title}</h4>\n {item.subtitle && <p className=\"text-xs text-text-secondary mt-0.5 line-clamp-2\">{item.subtitle}</p>}\n </div>\n\n {/* Value */}\n {item.value !== undefined && (\n <span className=\"flex-shrink-0 text-sm font-medium text-text-primary\">\n {typeof item.value === 'number' ? new Intl.NumberFormat('en-US').format(item.value) : item.value}\n </span>\n )}\n </div>\n\n {/* Metadata */}\n {item.metadata && item.metadata.length > 0 && (\n <div className=\"flex flex-wrap gap-x-4 gap-y-1 mt-2\">\n {item.metadata.map((meta, index) => (\n <span key={index} className=\"text-xs text-text-secondary\">\n <span className=\"text-text-secondary/70\">{meta.label}: </span>\n <span className=\"font-medium\">{meta.value}</span>\n </span>\n ))}\n </div>\n )}\n\n {/* Badge */}\n {item.badge && (\n <div className=\"mt-2\">\n <span\n className={`\n inline-flex items-center\n px-2 py-0.5\n text-xs font-medium\n rounded-full\n ${getBadgeColor(item.badge)}\n `}\n >\n {item.badge.text}\n </span>\n </div>\n )}\n </div>\n\n {/* Chevron for clickable items */}\n {isClickable && (\n <div className=\"flex-shrink-0 self-center\">\n <svg className=\"w-4 h-4 text-text-secondary\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </div>\n )}\n </div>\n );\n});\n\nexport default ListItem;\n"],"mappings":";;;AAoDA,SAAS,EAAc,GAAsC;AAE3D,SADc,GAAO,OACrB;EACE,KAAK,UACH,QAAO;EACT,KAAK,UACH,QAAO;EACT,KAAK,QACH,QAAO;EACT,KAAK,OACH,QAAO;EAET,QACE,QAAO;;;AAQb,IAAa,IAA8B,EAAK,SAAkB,EAChE,SACA,eAAY,IACZ,YACA,iBAAc,MACb;CACD,IAAM,IAAc,KAAa,CAAC,EAAK;AAEvC,QACE,kBAAC,OAAD;EACE,WAAW;;UAEP,IAAc,mCAAmC,GAAG;UACpD,IAAc,2DAA2D,GAAG;UAC5E,EAAK,WAAW,eAAe,GAAG;;EAEtC,eAAe;AACb,GAAI,KAAe,KACjB,GAAS;;EAGb,MAAM,IAAc,WAAW,KAAA;EAC/B,UAAU,IAAc,IAAI,KAAA;EAC5B,YAAY,MAAM;AAChB,GAAI,KAAe,MAAY,EAAE,QAAQ,WAAW,EAAE,QAAQ,SAC5D,EAAE,gBAAgB,EAClB,GAAS;;YAjBf;GAsBG,EAAK,QACJ,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAK;IACF,CAAA;GAIR,kBAAC,OAAD;IAAK,WAAU;cAAf;KAEE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAkD,EAAK;QAAW,CAAA,EAC/E,EAAK,YAAY,kBAAC,KAAD;QAAG,WAAU;kBAAmD,EAAK;QAAa,CAAA,CAChG;UAGL,EAAK,UAAU,KAAA,KACd,kBAAC,QAAD;OAAM,WAAU;iBACb,OAAO,EAAK,SAAU,WAAW,IAAI,KAAK,aAAa,QAAQ,CAAC,OAAO,EAAK,MAAM,GAAG,EAAK;OACtF,CAAA,CAEL;;KAGL,EAAK,YAAY,EAAK,SAAS,SAAS,KACvC,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAK,SAAS,KAAK,GAAM,MACxB,kBAAC,QAAD;OAAkB,WAAU;iBAA5B,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAA0C,EAAK,OAAM,KAAS;WAC9D,kBAAC,QAAD;QAAM,WAAU;kBAAe,EAAK;QAAa,CAAA,CAC5C;SAHI,EAGJ,CACP;MACE,CAAA;KAIP,EAAK,SACJ,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,QAAD;OACE,WAAW;;;;;kBAKP,EAAc,EAAK,MAAM,CAAC;;iBAG7B,EAAK,MAAM;OACP,CAAA;MACH,CAAA;KAEJ;;GAGL,KACC,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD;KAAK,WAAU;KAA8B,MAAK;KAAO,QAAO;KAAe,SAAQ;eACrF,kBAAC,QAAD;MAAM,eAAc;MAAQ,gBAAe;MAAQ,aAAa;MAAG,GAAE;MAAiB,CAAA;KAClF,CAAA;IACF,CAAA;GAEJ;;EAER"}
@@ -199,14 +199,14 @@ var y = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
199
199
  M,
200
200
  T
201
201
  ]), /* @__PURE__ */ o("div", {
202
- className: "relative h-full w-full overflow-hidden rounded-lg bg-bg-muted",
202
+ className: "relative h-full w-full overflow-hidden rounded-lg bg-bg-sunken",
203
203
  children: [
204
204
  /* @__PURE__ */ a("div", {
205
205
  ref: f,
206
206
  className: "h-full w-full"
207
207
  }),
208
208
  b !== "error" && E.length === 0 && !N && /* @__PURE__ */ a("div", {
209
- className: "pointer-events-none absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted/70 text-sm text-text-secondary",
209
+ className: "pointer-events-none absolute inset-0 z-[500] flex items-center justify-center bg-bg-sunken/70 text-sm text-text-secondary",
210
210
  children: /* @__PURE__ */ o("div", {
211
211
  className: "text-center",
212
212
  children: [/* @__PURE__ */ o("svg", {
@@ -229,7 +229,7 @@ var y = e(function({ widget: e, data: c, onClick: l, onDrilldown: u }) {
229
229
  })
230
230
  }),
231
231
  b === "error" && /* @__PURE__ */ a("div", {
232
- className: "absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted px-4 text-center text-sm text-text-secondary",
232
+ className: "absolute inset-0 z-[500] flex items-center justify-center bg-bg-sunken px-4 text-center text-sm text-text-secondary",
233
233
  children: "The map needs an internet connection and couldn’t load."
234
234
  })
235
235
  ]