@burdenoff/microfe-bigconsole 2026.624.1 → 2026.625.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bigconsole/BigConsoleRoutes.js +94 -94
- package/dist/bigconsole/BigConsoleRoutes.js.map +1 -1
- package/dist/bigconsole/assistant/assistantApi.js +215 -0
- package/dist/bigconsole/assistant/assistantApi.js.map +1 -0
- package/dist/bigconsole/assistant/createSandboxAssistantTransport.js +119 -0
- package/dist/bigconsole/assistant/createSandboxAssistantTransport.js.map +1 -0
- package/dist/bigconsole/assistant/index.js +2 -0
- package/dist/bigconsole/assistant/pageContext.js +51 -0
- package/dist/bigconsole/assistant/pageContext.js.map +1 -0
- package/dist/bigconsole/components/DrilldownModal.js +4 -4
- package/dist/bigconsole/components/DrilldownModal.js.map +1 -1
- package/dist/bigconsole/components/dashboard/CreateDashboardDialog.js +48 -48
- package/dist/bigconsole/components/dashboard/DashboardCanvas.js +1 -1
- package/dist/bigconsole/components/dashboard/DashboardCanvas.js.map +1 -1
- package/dist/bigconsole/components/dashboard/DashboardToolbar.js +54 -54
- package/dist/bigconsole/components/dashboard/DashboardToolbar.js.map +1 -1
- package/dist/bigconsole/components/dashboard/DrilldownBreadcrumb.js +1 -1
- package/dist/bigconsole/components/dashboard/DrilldownBreadcrumb.js.map +1 -1
- package/dist/bigconsole/components/dashboard/ExportDashboardDialog.js +33 -33
- package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js +205 -205
- package/dist/bigconsole/components/dashboard/ImportDashboardDialog.js.map +1 -1
- package/dist/bigconsole/components/dashboard/WidgetPalette.js +38 -38
- package/dist/bigconsole/components/datasink/CacheTTLSelector.js +1 -1
- package/dist/bigconsole/components/datasink/CacheTTLSelector.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkCard.js +4 -4
- package/dist/bigconsole/components/datasink/DataSinkCard.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkDataViewer.js +2 -2
- package/dist/bigconsole/components/datasink/DataSinkDataViewer.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js +2 -2
- package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkRefreshDialog.js +2 -2
- package/dist/bigconsole/components/datasink/DataSinkRefreshDialog.js.map +1 -1
- package/dist/bigconsole/components/datasink/DataSinkTableViewer.js +34 -34
- package/dist/bigconsole/components/datasink/DataSinkTableViewer.js.map +1 -1
- package/dist/bigconsole/components/datasink/ExportDataSinkDialog.js +31 -31
- package/dist/bigconsole/components/datasink/ImportDataSinkDialog.js +121 -121
- package/dist/bigconsole/components/datasink/ImportDataSinkDialog.js.map +1 -1
- package/dist/bigconsole/components/datasink/LinkedParsersPanel.js +1 -1
- package/dist/bigconsole/components/datasink/LinkedParsersPanel.js.map +1 -1
- package/dist/bigconsole/components/datasink/LinkedWorkflowsPanel.js +3 -3
- package/dist/bigconsole/components/datasink/LinkedWorkflowsPanel.js.map +1 -1
- package/dist/bigconsole/components/datasink/RefreshTypeSelector.js +2 -2
- package/dist/bigconsole/components/datasink/RefreshTypeSelector.js.map +1 -1
- package/dist/bigconsole/components/datasink/WorkflowStatusPanel.js +2 -2
- package/dist/bigconsole/components/datasink/WorkflowStatusPanel.js.map +1 -1
- package/dist/bigconsole/components/parser/DataSinkSelector.js +3 -3
- package/dist/bigconsole/components/parser/DataSinkSelector.js.map +1 -1
- package/dist/bigconsole/components/parser/ExportParserDialog.js +31 -31
- package/dist/bigconsole/components/parser/ImportParserDialog.js +186 -186
- package/dist/bigconsole/components/parser/ImportParserDialog.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserCard.js +5 -5
- package/dist/bigconsole/components/parser/ParserCard.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserSelector.js +6 -6
- package/dist/bigconsole/components/parser/ParserSelector.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserStatusBadge.js +1 -1
- package/dist/bigconsole/components/parser/ParserStatusBadge.js.map +1 -1
- package/dist/bigconsole/components/parser/ParserTypeBadge.js +1 -1
- package/dist/bigconsole/components/parser/ParserTypeBadge.js.map +1 -1
- package/dist/bigconsole/components/pipeline/PipelineCard.js +2 -2
- package/dist/bigconsole/components/pipeline/PipelineCard.js.map +1 -1
- package/dist/bigconsole/components/pipeline/PipelineDeleteDialog.js +1 -1
- package/dist/bigconsole/components/pipeline/PipelineDeleteDialog.js.map +1 -1
- package/dist/bigconsole/components/pipeline/PipelineStatusBadge.js +1 -1
- package/dist/bigconsole/components/pipeline/PipelineStatusBadge.js.map +1 -1
- package/dist/bigconsole/components/widgets/ActionConfigDialog.js +79 -79
- package/dist/bigconsole/components/widgets/ActionConfigDialog.js.map +1 -1
- package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js +11 -11
- package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js.map +1 -1
- package/dist/bigconsole/components/widgets/PropertiesPanel.js +322 -322
- package/dist/bigconsole/components/widgets/PropertiesPanel.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetConfigDialog.js +234 -234
- package/dist/bigconsole/components/widgets/WidgetConfigDialog.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetHeader.js +4 -4
- package/dist/bigconsole/components/widgets/WidgetHeader.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetMenu.js +3 -3
- package/dist/bigconsole/components/widgets/WidgetMenu.js.map +1 -1
- package/dist/bigconsole/components/widgets/WidgetTypeSelector.js +23 -23
- package/dist/bigconsole/components/widgets/WidgetWrapper.js +1 -1
- package/dist/bigconsole/components/widgets/WidgetWrapper.js.map +1 -1
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js +1 -1
- package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js +2 -2
- package/dist/bigconsole/components/widgets/comments/WidgetComments.js.map +1 -1
- package/dist/bigconsole/components/widgets/custom/CustomWidget.js +5 -5
- package/dist/bigconsole/components/widgets/custom/CustomWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js +1 -1
- package/dist/bigconsole/components/widgets/gauge/GaugeWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/gauge/LinearGauge.js +1 -1
- package/dist/bigconsole/components/widgets/gauge/LinearGauge.js.map +1 -1
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +2 -2
- package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/kpi/KPIWidget.js +2 -2
- package/dist/bigconsole/components/widgets/kpi/KPIWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js +2 -2
- package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/list/ListItem.js +3 -3
- package/dist/bigconsole/components/widgets/list/ListItem.js.map +1 -1
- package/dist/bigconsole/components/widgets/map-widget/MapWidget.js +3 -3
- package/dist/bigconsole/components/widgets/map-widget/MapWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/ComparisonBadge.js +1 -1
- package/dist/bigconsole/components/widgets/metric-card/ComparisonBadge.js.map +1 -1
- package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +2 -2
- package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js +12 -12
- package/dist/bigconsole/components/widgets/pivot-table/PivotTableWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/progress/ProgressBar.js +2 -2
- package/dist/bigconsole/components/widgets/progress/ProgressBar.js.map +1 -1
- package/dist/bigconsole/components/widgets/progress/ProgressConfig.js +1 -1
- package/dist/bigconsole/components/widgets/progress/ProgressConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/progress/ProgressWidget.js +1 -1
- package/dist/bigconsole/components/widgets/progress/ProgressWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/retention/RetentionWidget.js +2 -2
- package/dist/bigconsole/components/widgets/retention/RetentionWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/states/EmptyState.js +1 -1
- package/dist/bigconsole/components/widgets/states/EmptyState.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableCell.js +1 -1
- package/dist/bigconsole/components/widgets/table/TableCell.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableConfig.js +1 -1
- package/dist/bigconsole/components/widgets/table/TableConfig.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableHeader.js +2 -2
- package/dist/bigconsole/components/widgets/table/TableHeader.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TablePagination.js +4 -4
- package/dist/bigconsole/components/widgets/table/TablePagination.js.map +1 -1
- package/dist/bigconsole/components/widgets/table/TableWidget.js +5 -5
- package/dist/bigconsole/components/widgets/table/TableWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/text-widget/TextWidget.js +1 -1
- package/dist/bigconsole/components/widgets/text-widget/TextWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js +3 -3
- package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js.map +1 -1
- package/dist/bigconsole/components/widgets/utils/conditionalFormat.js +2 -2
- package/dist/bigconsole/components/widgets/utils/conditionalFormat.js.map +1 -1
- package/dist/bigconsole/components/widgets/widget-actions/WidgetActions.js +3 -3
- package/dist/bigconsole/components/widgets/widget-actions/WidgetActions.js.map +1 -1
- package/dist/bigconsole/components/workflow/WorkflowCard.js +2 -2
- package/dist/bigconsole/components/workflow/WorkflowCard.js.map +1 -1
- package/dist/bigconsole/components/workflow/WorkflowDeleteDialog.js +1 -1
- package/dist/bigconsole/components/workflow/WorkflowDeleteDialog.js.map +1 -1
- package/dist/bigconsole/index.js +3 -0
- package/dist/bigconsole/pages/DashboardBuilderPage.js +76 -76
- package/dist/bigconsole/pages/DashboardBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/DashboardViewPage.js +169 -190
- package/dist/bigconsole/pages/DashboardViewPage.js.map +1 -1
- package/dist/bigconsole/pages/DashboardsPage.js +268 -312
- package/dist/bigconsole/pages/DashboardsPage.js.map +1 -1
- package/dist/bigconsole/pages/DataParsersPage.js +148 -194
- package/dist/bigconsole/pages/DataParsersPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSinkBuilderPage.js +254 -248
- package/dist/bigconsole/pages/DataSinkBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSinkViewPage.js +256 -231
- package/dist/bigconsole/pages/DataSinkViewPage.js.map +1 -1
- package/dist/bigconsole/pages/DataSinksPage.js +173 -219
- package/dist/bigconsole/pages/DataSinksPage.js.map +1 -1
- package/dist/bigconsole/pages/HomePage.js +125 -141
- package/dist/bigconsole/pages/HomePage.js.map +1 -1
- package/dist/bigconsole/pages/InsightsPage.js +159 -184
- package/dist/bigconsole/pages/InsightsPage.js.map +1 -1
- package/dist/bigconsole/pages/ParserBuilderPage.js +404 -396
- package/dist/bigconsole/pages/ParserBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/ParserViewPage.js +221 -202
- package/dist/bigconsole/pages/ParserViewPage.js.map +1 -1
- package/dist/bigconsole/pages/PipelineBuilderPage.js +216 -210
- package/dist/bigconsole/pages/PipelineBuilderPage.js.map +1 -1
- package/dist/bigconsole/pages/PipelineViewPage.js +213 -190
- package/dist/bigconsole/pages/PipelineViewPage.js.map +1 -1
- package/dist/bigconsole/pages/PipelinesPage.js +107 -150
- package/dist/bigconsole/pages/PipelinesPage.js.map +1 -1
- package/dist/bigconsole/pages/SettingsPage.js +184 -171
- package/dist/bigconsole/pages/SettingsPage.js.map +1 -1
- package/dist/bigconsole/pages/WidgetDemoPage.js +71 -83
- package/dist/bigconsole/pages/WidgetDemoPage.js.map +1 -1
- package/dist/bigconsole/pages/WorkflowViewPage.js +235 -221
- package/dist/bigconsole/pages/WorkflowViewPage.js.map +1 -1
- package/dist/bigconsole/pages/WorkflowsPage.js +106 -149
- package/dist/bigconsole/pages/WorkflowsPage.js.map +1 -1
- package/dist/bigconsole/renderers/adapters/AdaptiveCardRenderer.js +12 -12
- package/dist/bigconsole/renderers/adapters/AdaptiveCardRenderer.js.map +1 -1
- package/dist/bigconsole/renderers/adapters/BigConsoleRenderer.js +2 -2
- package/dist/bigconsole/renderers/adapters/BigConsoleRenderer.js.map +1 -1
- package/dist/bigconsole/renderers/adapters/BlockKitRenderer.js +5 -5
- package/dist/bigconsole/renderers/adapters/BlockKitRenderer.js.map +1 -1
- package/dist/bigconsole/renderers/adapters/CustomRenderer.js +2 -2
- package/dist/bigconsole/renderers/adapters/CustomRenderer.js.map +1 -1
- package/dist/generated/wspace-operations.js +87 -36
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/index.js +3 -1
- package/package.json +6 -2
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MapWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/map-widget/MapWidget.tsx"],"sourcesContent":["/**\n * MapWidget Component\n *\n * Interactive geographic map (Leaflet + OpenStreetMap tiles) with data-bound\n * markers. Leaflet's stylesheet is loaded from a CDN at runtime (with SRI) because\n * the build-time MFE library bundle does not ship a component's CSS to the host\n * shell — consistent with the map being an inherently online feature (tiles).\n */\n\nimport { type FC, memo, useEffect, useMemo, useRef, useState } from 'react';\nimport L from 'leaflet';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MapWidgetProps {\n widget: Widget;\n data?: MapData;\n onClick?: (marker: MapMarker) => void;\n /** Drilldown handler - receives selected marker data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MapMarker {\n name: string;\n lat: number;\n lng: number;\n value?: number;\n popup?: string;\n color?: string;\n}\n\n/** Minimal GeoJSON FeatureCollection shape (we only read geometry + properties). */\nexport interface GeoJsonFeatureCollection {\n type: 'FeatureCollection';\n features: Array<{\n type: 'Feature';\n id?: string | number;\n properties?: Record<string, unknown> | null;\n geometry: unknown;\n }>;\n}\n\n/** A region value used to shade a choropleth (joined to a GeoJSON feature). */\nexport interface MapRegionValue {\n /** Join key — matched against the feature's id field. */\n id: string;\n /** Numeric value driving the fill intensity. */\n value: number;\n /** Optional display name for the popup. */\n name?: string;\n}\n\nexport interface MapData {\n center?: { lat: number; lng: number };\n zoom?: number;\n markers?: MapMarker[];\n /** GeoJSON regions for choropleth mode. */\n geoJson?: GeoJsonFeatureCollection;\n /** Per-region values joined to GeoJSON features for choropleth shading. */\n regions?: MapRegionValue[];\n}\n\n// ============================================================================\n// Leaflet CSS (runtime CDN injection)\n// ============================================================================\n\nconst LEAFLET_CSS = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';\n// SRI for leaflet@1.9.4/dist/leaflet.css (immutable versioned URL) — guards against a CDN compromise.\nconst LEAFLET_CSS_SRI = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';\n\nlet leafletCssRequested = false;\n\nfunction ensureLeafletCss(): void {\n // Module-level guard in addition to the DOM check so two MapWidgets mounting in\n // the same tick can't both append the <link> before either lands in the DOM.\n if (leafletCssRequested || document.getElementById('leaflet-css')) return;\n leafletCssRequested = true;\n const link = document.createElement('link');\n link.id = 'leaflet-css';\n link.rel = 'stylesheet';\n link.href = LEAFLET_CSS;\n link.integrity = LEAFLET_CSS_SRI;\n link.crossOrigin = 'anonymous';\n // Don't leak the current path (workspace/dashboard IDs) to the CDN.\n link.referrerPolicy = 'no-referrer';\n // Leaflet needs its stylesheet for correct tile/marker positioning. If the CDN\n // is unreachable or the SRI check fails the load is otherwise silent, so warn and\n // clear the guard/element to allow a later mount to retry.\n link.onerror = () => {\n console.warn('[bigconsole] Leaflet CSS failed to load; map styling may be broken.');\n leafletCssRequested = false;\n link.remove();\n };\n document.head.appendChild(link);\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst DEFAULT_MARKER_COLOR = 'var(--color-chart-1, #3b82f6)';\n\n/**\n * Marker colours come from widget data and are interpolated into an inline\n * `style` attribute, so restrict them to a safe character set (hex / rgb() /\n * hsl() / css var() / named colours). Anything outside that — or containing\n * style-breaking characters — falls back to the default token colour.\n */\nfunction safeColor(color: unknown): string {\n // Marker data is untrusted: a parser can emit a non-string colour (e.g. a numeric\n // status code) that slips past the lat/lng filter, so guard the type before trim().\n if (typeof color !== 'string') return DEFAULT_MARKER_COLOR;\n const trimmed = color.trim();\n if (trimmed.length > 64) return DEFAULT_MARKER_COLOR;\n if (/[;{}<>\"'\\\\]/.test(trimmed)) return DEFAULT_MARKER_COLOR;\n // Block `url()` so a marker colour can't smuggle an exfiltration beacon\n // (e.g. `url(//evil.com/pixel)`) through the otherwise-permissive char set.\n if (/\\burl\\s*\\(/i.test(trimmed)) return DEFAULT_MARKER_COLOR;\n // Allow `_` (custom-property names) and `/` (modern `rgb(r g b / a)` syntax);\n // the dangerous-char guard above still blocks anything that could break out of\n // the inline style.\n if (!/^[#a-zA-Z0-9(),.%/_\\s-]+$/.test(trimmed)) return DEFAULT_MARKER_COLOR;\n return trimmed;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === 'number' && Number.isFinite(v);\n}\n\n/**\n * Theme-safe cold→hot ramp for the heat-point layer: normalized intensity\n * (0–1) → info → warning → error status tokens, returned as `var()` refs.\n *\n * We deliberately pass the `var()` reference (not a resolved color): the map\n * uses Leaflet's default SVG renderer, where `fill` participates in the CSS\n * cascade, so the tokens resolve correctly AND stay live across theme toggles\n * (a resolved concrete color would go stale until the next data refresh). This\n * also matches the choropleth layer and avoids any per-marker `getComputedStyle`\n * lookup. (If the map ever opts into `preferCanvas`, pre-resolve these instead.)\n */\nfunction heatColor(norm: number): string {\n if (norm >= 0.66) return 'var(--color-status-error)';\n if (norm >= 0.33) return 'var(--color-status-warning)';\n return 'var(--color-status-info)';\n}\n\n/**\n * Build a marker/region popup as DOM (a bold title + optional formatted value).\n * Uses textContent — never an HTML string — so marker-supplied text can't be\n * interpreted as markup. Shared by the pin and heat-point branches.\n */\nfunction buildMarkerPopup(marker: MapMarker): HTMLDivElement {\n const popupEl = document.createElement('div');\n const title = document.createElement('div');\n title.style.fontWeight = '600';\n title.textContent = marker.popup || marker.name || 'Location';\n popupEl.appendChild(title);\n if (isFiniteNumber(marker.value)) {\n const val = document.createElement('div');\n val.textContent = marker.value.toLocaleString();\n popupEl.appendChild(val);\n }\n return popupEl;\n}\n\n/**\n * Normalize the bound widget data into markers + optional center/zoom. Accepts\n * the documented `{ markers, center, zoom }` shape, or a bare array of markers\n * (parser output), and drops any entry without valid coordinates.\n */\ninterface NormalizedMapData {\n markers: MapMarker[];\n /** Center explicitly supplied by the data payload — wins over markers. */\n center?: { lat: number; lng: number };\n /** Zoom explicitly supplied by the data payload. */\n zoom?: number;\n /** Center from widget config — a static fallback used only when there are no markers. */\n fallbackCenter?: { lat: number; lng: number };\n /** Zoom from widget config — fallback for the no-marker case. */\n fallbackZoom?: number;\n /** GeoJSON for choropleth mode. */\n geoJson?: GeoJsonFeatureCollection;\n /** region id → { value, name } map for choropleth shading. */\n regionValues: Map<string, { value: number; name?: string }>;\n}\n\nfunction asLatLng(v: unknown): { lat: number; lng: number } | undefined {\n const c = v as { lat?: unknown; lng?: unknown } | undefined;\n return c && isFiniteNumber(c.lat) && isFiniteNumber(c.lng) ? { lat: c.lat, lng: c.lng } : undefined;\n}\n\nfunction normalizeMapData(data: MapData | MapMarker[] | undefined, config: Record<string, unknown>): NormalizedMapData {\n // The widget may be handed either the documented { markers, center, zoom } shape\n // or a bare marker array (parser output); narrow once so the rest is type-safe.\n const payload: MapData | undefined = Array.isArray(data) ? undefined : data;\n const rawMarkers: unknown = Array.isArray(data) ? data : data?.markers;\n const markers: MapMarker[] = Array.isArray(rawMarkers)\n ? (rawMarkers as MapMarker[]).filter((m) => m && isFiniteNumber(m.lat) && isFiniteNumber(m.lng))\n : [];\n\n // Choropleth inputs: GeoJSON (from data payload or config) + a region→value join.\n const geoJson = (payload?.geoJson ?? (config.geoJson as GeoJsonFeatureCollection | undefined)) || undefined;\n const regionIdField = (config.regionIdField as string) || 'id';\n const regionValues = new Map<string, { value: number; name?: string }>();\n const rawRegions: unknown = payload?.regions ?? config.regions;\n if (Array.isArray(rawRegions)) {\n for (const r of rawRegions as Array<Record<string, unknown>>) {\n if (!r) continue;\n const id = r[regionIdField] ?? r.id ?? r.region ?? r.code;\n const value = r.value ?? r.count ?? r.total;\n if (id != null && isFiniteNumber(value)) {\n regionValues.set(String(id), { value, name: typeof r.name === 'string' ? r.name : undefined });\n }\n }\n }\n\n // Data-payload center/zoom are an explicit intent and win over markers. Config\n // center/zoom (e.g. the WidgetRegistry default of {0,0}/2) are only a static\n // fallback for marker-less maps — otherwise that default would pin every marker\n // dataset to the world view instead of framing the markers.\n return {\n markers,\n center: asLatLng(payload?.center),\n zoom: isFiniteNumber(payload?.zoom) ? payload?.zoom : undefined,\n fallbackCenter: asLatLng(config.center),\n fallbackZoom: isFiniteNumber(config.zoom) ? (config.zoom as number) : undefined,\n geoJson: geoJson && Array.isArray(geoJson.features) ? geoJson : undefined,\n regionValues,\n };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MapWidget: FC<MapWidgetProps> = memo(function MapWidget({ widget, data, onClick, onDrilldown }) {\n const containerRef = useRef<HTMLDivElement>(null);\n const mapRef = useRef<L.Map | null>(null);\n // Signature of the last view we auto-framed to; lets us re-render markers on\n // every data change without snapping the viewport back when the framing\n // inputs (coords/center/zoom) are unchanged — e.g. a refetch of identical data.\n const lastFrameSigRef = useRef<string | null>(null);\n const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading');\n\n const config = useMemo(() => (widget?.config ?? {}) as Record<string, unknown>, [widget?.config]);\n const showMarkers = config.showMarkers !== false;\n // Heat-point density mode: render weighted, translucent circles whose overlap\n // accumulates into a heat-like surface (dependency-free — no leaflet.heat).\n const heatMode = config.mapMode === 'heat';\n const regionIdField = (config.regionIdField as string) || 'id';\n const { markers, center, zoom, fallbackCenter, fallbackZoom, geoJson, regionValues } = useMemo(\n () => normalizeMapData(data, config),\n [data, config]\n );\n const hasChoropleth = Boolean(geoJson && regionValues.size > 0);\n\n // Keep the latest click handler in a ref so marker bindings don't need rebinding.\n // Assign in an effect (not during render) per the React 19 useRef guidance.\n const clickRef = useRef<(marker: MapMarker) => void>(() => undefined);\n useEffect(() => {\n clickRef.current = (marker: MapMarker) => {\n if (onDrilldown) onDrilldown(marker as unknown as Record<string, unknown>);\n else if (onClick) onClick(marker);\n };\n }, [onClick, onDrilldown]);\n\n // Initialise the map once.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return undefined;\n ensureLeafletCss();\n\n let map: L.Map;\n try {\n map = L.map(el, { center: [20, 0], zoom: 2, scrollWheelZoom: true, attributionControl: true });\n const tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© OpenStreetMap contributors',\n maxZoom: 18,\n });\n // Leaflet reports tile failures asynchronously, so addTo() won't throw when\n // tiles are blocked/offline. Surface the error overlay only if a tile errors\n // before any tile has loaded (true offline/blocked), tolerating the odd\n // transient miss once the base map is up.\n let anyTileLoaded = false;\n tiles.on('tileload', () => {\n anyTileLoaded = true;\n // Recover if an early tile miss had shown the offline overlay — a flaky\n // connection that later succeeds shouldn't stay hidden behind it.\n setStatus((s) => (s === 'error' ? 'ready' : s));\n });\n tiles.on('tileerror', () => {\n if (!anyTileLoaded) setStatus('error');\n });\n tiles.addTo(map);\n mapRef.current = map;\n // Fresh map ⇒ force the next marker effect to (re)frame it. Without this the\n // frame-signature ref would survive a remount (e.g. React StrictMode's\n // double-invoke) and the new map would stay stuck at this init view.\n lastFrameSigRef.current = null;\n setStatus('ready');\n } catch {\n setStatus('error');\n return undefined;\n }\n\n // Widgets live in a resizable grid; keep tiles correct as the box changes.\n const ro =\n typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => mapRef.current?.invalidateSize()) : undefined;\n ro?.observe(el);\n const t = window.setTimeout(() => map.invalidateSize(), 80);\n\n return () => {\n window.clearTimeout(t);\n ro?.disconnect();\n map.remove();\n mapRef.current = null;\n };\n }, []);\n\n // Render markers + frame the view whenever the data changes.\n useEffect(() => {\n const map = mapRef.current;\n if (!map) return undefined;\n\n const layer = L.layerGroup();\n\n // Choropleth: shade GeoJSON regions by a single-hue intensity ramp (the\n // semantic `info` token at varying opacity) joined on the region id field.\n let geoLayer: L.GeoJSON | null = null;\n if (geoJson && regionValues.size > 0) {\n const vals = Array.from(regionValues.values()).map((v) => v.value);\n const min = Math.min(...vals);\n const max = Math.max(...vals);\n const span = max - min || 1;\n const FILL = 'var(--color-status-info, #3b82f6)';\n\n const lookup = (feature: { id?: string | number; properties?: Record<string, unknown> | null }) => {\n const props = feature.properties ?? {};\n const joinId = feature.id ?? props[regionIdField] ?? props.id ?? props.code;\n return joinId != null ? regionValues.get(String(joinId)) : undefined;\n };\n\n geoLayer = L.geoJSON(geoJson as unknown as Parameters<typeof L.geoJSON>[0], {\n style: (feature) => {\n const hit = feature ? lookup(feature) : undefined;\n const norm = hit ? (hit.value - min) / span : 0;\n return {\n fillColor: FILL,\n fillOpacity: hit ? 0.2 + norm * 0.65 : 0.05,\n color: 'var(--color-border-default, #cbd5e1)',\n weight: 1,\n };\n },\n onEachFeature: (feature, lyr) => {\n const hit = lookup(feature);\n const name =\n hit?.name ??\n (typeof feature.properties?.name === 'string' ? (feature.properties.name as string) : undefined) ??\n String(feature.id ?? '');\n const popupEl = document.createElement('div');\n const title = document.createElement('div');\n title.style.fontWeight = '600';\n title.textContent = name || 'Region';\n popupEl.appendChild(title);\n if (hit) {\n const val = document.createElement('div');\n val.textContent = hit.value.toLocaleString();\n popupEl.appendChild(val);\n }\n lyr.bindPopup(popupEl);\n lyr.on('click', () => clickRef.current({ name, lat: NaN, lng: NaN, value: hit?.value } as MapMarker));\n },\n });\n geoLayer.addTo(layer);\n }\n\n if (showMarkers && heatMode) {\n // Heat-point layer: translucent weighted circles, sized + colored by value.\n const weights = markers.map((mk) => (isFiniteNumber(mk.value) ? mk.value : 1));\n // reduce, not Math.min(...spread) — spreading a large array as args can\n // blow the call-stack (RangeError) for big datasets.\n const minW = weights.reduce((a, b) => Math.min(a, b), Infinity);\n const maxW = weights.reduce((a, b) => Math.max(a, b), -Infinity);\n const spanW = maxW - minW || 1;\n markers.forEach((marker, i) => {\n const norm = (weights[i] - minW) / spanW;\n const circle = L.circleMarker([marker.lat, marker.lng], {\n radius: 10 + norm * 22,\n fillColor: heatColor(norm),\n // Both radius and opacity ramp with intensity for a truer density look.\n fillOpacity: 0.25 + norm * 0.35,\n stroke: false,\n }).addTo(layer);\n circle.bindPopup(buildMarkerPopup(marker));\n circle.on('click', () => clickRef.current(marker));\n });\n } else if (showMarkers) {\n for (const marker of markers) {\n // Build the pin as a DOM element (not an HTML string) so the marker colour\n // is assigned via the style API and can never be interpreted as markup —\n // defense-in-depth alongside safeColor().\n const pin = document.createElement('div');\n Object.assign(pin.style, {\n width: '14px',\n height: '14px',\n borderRadius: '50% 50% 50% 0',\n transform: 'rotate(-45deg)',\n background: safeColor(marker.color),\n border: '2px solid #fff',\n boxShadow: '0 1px 4px rgba(0,0,0,.4)',\n });\n const icon = L.divIcon({\n className: 'bc-map-marker',\n html: pin,\n iconSize: [14, 14],\n iconAnchor: [7, 14],\n });\n const m = L.marker([marker.lat, marker.lng], { icon }).addTo(layer);\n m.bindPopup(buildMarkerPopup(marker));\n m.on('click', () => clickRef.current(marker));\n }\n }\n layer.addTo(map);\n\n // Only (re)frame the view when the framing inputs actually change — otherwise\n // a data-prop reference change (e.g. a refetch returning identical markers)\n // would discard the user's current pan/zoom.\n const frameSig = JSON.stringify({\n m: markers.map((mk) => [mk.lat, mk.lng]),\n c: center ? [center.lat, center.lng] : null,\n z: zoom ?? null,\n fc: fallbackCenter ? [fallbackCenter.lat, fallbackCenter.lng] : null,\n fz: fallbackZoom ?? null,\n g: geoJson ? regionValues.size : 0,\n });\n if (frameSig !== lastFrameSigRef.current) {\n lastFrameSigRef.current = frameSig;\n // Framing priority: data-payload center → frame the markers → frame the\n // choropleth regions → config fallback center → zoom only → world view.\n // Markers/regions win over the config default so a palette-created map frames\n // its data instead of {0,0}.\n if (center) {\n map.setView([center.lat, center.lng], zoom ?? map.getZoom());\n } else if (!markers.length && geoLayer) {\n const b = geoLayer.getBounds();\n if (b.isValid()) map.fitBounds(b, { padding: [24, 24], maxZoom: 12 });\n } else if (markers.length === 1) {\n map.setView([markers[0].lat, markers[0].lng], zoom ?? 8);\n } else if (markers.length > 1) {\n const bounds = L.latLngBounds(markers.map((m) => [m.lat, m.lng] as [number, number]));\n map.fitBounds(bounds, { padding: [32, 32], maxZoom: 12 });\n } else if (fallbackCenter) {\n map.setView([fallbackCenter.lat, fallbackCenter.lng], zoom ?? fallbackZoom ?? map.getZoom());\n } else if (isFiniteNumber(zoom ?? fallbackZoom)) {\n map.setZoom((zoom ?? fallbackZoom) as number);\n }\n }\n\n const resize = window.setTimeout(() => map.invalidateSize(), 0);\n\n return () => {\n window.clearTimeout(resize);\n layer.remove();\n };\n }, [\n markers,\n center,\n zoom,\n fallbackCenter,\n fallbackZoom,\n showMarkers,\n heatMode,\n geoJson,\n regionValues,\n regionIdField,\n ]);\n\n return (\n <div className=\"relative h-full w-full overflow-hidden rounded-lg bg-bg-muted\">\n <div ref={containerRef} className=\"h-full w-full\" />\n\n {status !== 'error' && markers.length === 0 && !hasChoropleth && (\n <div className=\"pointer-events-none absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted/70 text-sm text-text-secondary\">\n <div className=\"text-center\">\n <svg\n className=\"mx-auto mb-2 h-12 w-12 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"\n />\n </svg>\n <p>No map data</p>\n </div>\n </div>\n )}\n\n {status === 'error' && (\n <div className=\"absolute inset-0 z-[500] flex items-center justify-center bg-bg-muted px-4 text-center text-sm text-text-secondary\">\n The map needs an internet connection and couldn’t load.\n </div>\n )}\n </div>\n );\n});\n\nexport default MapWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MapWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/map-widget/MapWidget.tsx"],"sourcesContent":["/**\n * MapWidget Component\n *\n * Interactive geographic map (Leaflet + OpenStreetMap tiles) with data-bound\n * markers. Leaflet's stylesheet is loaded from a CDN at runtime (with SRI) because\n * the build-time MFE library bundle does not ship a component's CSS to the host\n * shell — consistent with the map being an inherently online feature (tiles).\n */\n\nimport { type FC, memo, useEffect, useMemo, useRef, useState } from 'react';\nimport L from 'leaflet';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MapWidgetProps {\n widget: Widget;\n data?: MapData;\n onClick?: (marker: MapMarker) => void;\n /** Drilldown handler - receives selected marker data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MapMarker {\n name: string;\n lat: number;\n lng: number;\n value?: number;\n popup?: string;\n color?: string;\n}\n\n/** Minimal GeoJSON FeatureCollection shape (we only read geometry + properties). */\nexport interface GeoJsonFeatureCollection {\n type: 'FeatureCollection';\n features: Array<{\n type: 'Feature';\n id?: string | number;\n properties?: Record<string, unknown> | null;\n geometry: unknown;\n }>;\n}\n\n/** A region value used to shade a choropleth (joined to a GeoJSON feature). */\nexport interface MapRegionValue {\n /** Join key — matched against the feature's id field. */\n id: string;\n /** Numeric value driving the fill intensity. */\n value: number;\n /** Optional display name for the popup. */\n name?: string;\n}\n\nexport interface MapData {\n center?: { lat: number; lng: number };\n zoom?: number;\n markers?: MapMarker[];\n /** GeoJSON regions for choropleth mode. */\n geoJson?: GeoJsonFeatureCollection;\n /** Per-region values joined to GeoJSON features for choropleth shading. */\n regions?: MapRegionValue[];\n}\n\n// ============================================================================\n// Leaflet CSS (runtime CDN injection)\n// ============================================================================\n\nconst LEAFLET_CSS = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';\n// SRI for leaflet@1.9.4/dist/leaflet.css (immutable versioned URL) — guards against a CDN compromise.\nconst LEAFLET_CSS_SRI = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';\n\nlet leafletCssRequested = false;\n\nfunction ensureLeafletCss(): void {\n // Module-level guard in addition to the DOM check so two MapWidgets mounting in\n // the same tick can't both append the <link> before either lands in the DOM.\n if (leafletCssRequested || document.getElementById('leaflet-css')) return;\n leafletCssRequested = true;\n const link = document.createElement('link');\n link.id = 'leaflet-css';\n link.rel = 'stylesheet';\n link.href = LEAFLET_CSS;\n link.integrity = LEAFLET_CSS_SRI;\n link.crossOrigin = 'anonymous';\n // Don't leak the current path (workspace/dashboard IDs) to the CDN.\n link.referrerPolicy = 'no-referrer';\n // Leaflet needs its stylesheet for correct tile/marker positioning. If the CDN\n // is unreachable or the SRI check fails the load is otherwise silent, so warn and\n // clear the guard/element to allow a later mount to retry.\n link.onerror = () => {\n console.warn('[bigconsole] Leaflet CSS failed to load; map styling may be broken.');\n leafletCssRequested = false;\n link.remove();\n };\n document.head.appendChild(link);\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst DEFAULT_MARKER_COLOR = 'var(--color-chart-1, #3b82f6)';\n\n/**\n * Marker colours come from widget data and are interpolated into an inline\n * `style` attribute, so restrict them to a safe character set (hex / rgb() /\n * hsl() / css var() / named colours). Anything outside that — or containing\n * style-breaking characters — falls back to the default token colour.\n */\nfunction safeColor(color: unknown): string {\n // Marker data is untrusted: a parser can emit a non-string colour (e.g. a numeric\n // status code) that slips past the lat/lng filter, so guard the type before trim().\n if (typeof color !== 'string') return DEFAULT_MARKER_COLOR;\n const trimmed = color.trim();\n if (trimmed.length > 64) return DEFAULT_MARKER_COLOR;\n if (/[;{}<>\"'\\\\]/.test(trimmed)) return DEFAULT_MARKER_COLOR;\n // Block `url()` so a marker colour can't smuggle an exfiltration beacon\n // (e.g. `url(//evil.com/pixel)`) through the otherwise-permissive char set.\n if (/\\burl\\s*\\(/i.test(trimmed)) return DEFAULT_MARKER_COLOR;\n // Allow `_` (custom-property names) and `/` (modern `rgb(r g b / a)` syntax);\n // the dangerous-char guard above still blocks anything that could break out of\n // the inline style.\n if (!/^[#a-zA-Z0-9(),.%/_\\s-]+$/.test(trimmed)) return DEFAULT_MARKER_COLOR;\n return trimmed;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === 'number' && Number.isFinite(v);\n}\n\n/**\n * Theme-safe cold→hot ramp for the heat-point layer: normalized intensity\n * (0–1) → info → warning → error status tokens, returned as `var()` refs.\n *\n * We deliberately pass the `var()` reference (not a resolved color): the map\n * uses Leaflet's default SVG renderer, where `fill` participates in the CSS\n * cascade, so the tokens resolve correctly AND stay live across theme toggles\n * (a resolved concrete color would go stale until the next data refresh). This\n * also matches the choropleth layer and avoids any per-marker `getComputedStyle`\n * lookup. (If the map ever opts into `preferCanvas`, pre-resolve these instead.)\n */\nfunction heatColor(norm: number): string {\n if (norm >= 0.66) return 'var(--color-status-error)';\n if (norm >= 0.33) return 'var(--color-status-warning)';\n return 'var(--color-status-info)';\n}\n\n/**\n * Build a marker/region popup as DOM (a bold title + optional formatted value).\n * Uses textContent — never an HTML string — so marker-supplied text can't be\n * interpreted as markup. Shared by the pin and heat-point branches.\n */\nfunction buildMarkerPopup(marker: MapMarker): HTMLDivElement {\n const popupEl = document.createElement('div');\n const title = document.createElement('div');\n title.style.fontWeight = '600';\n title.textContent = marker.popup || marker.name || 'Location';\n popupEl.appendChild(title);\n if (isFiniteNumber(marker.value)) {\n const val = document.createElement('div');\n val.textContent = marker.value.toLocaleString();\n popupEl.appendChild(val);\n }\n return popupEl;\n}\n\n/**\n * Normalize the bound widget data into markers + optional center/zoom. Accepts\n * the documented `{ markers, center, zoom }` shape, or a bare array of markers\n * (parser output), and drops any entry without valid coordinates.\n */\ninterface NormalizedMapData {\n markers: MapMarker[];\n /** Center explicitly supplied by the data payload — wins over markers. */\n center?: { lat: number; lng: number };\n /** Zoom explicitly supplied by the data payload. */\n zoom?: number;\n /** Center from widget config — a static fallback used only when there are no markers. */\n fallbackCenter?: { lat: number; lng: number };\n /** Zoom from widget config — fallback for the no-marker case. */\n fallbackZoom?: number;\n /** GeoJSON for choropleth mode. */\n geoJson?: GeoJsonFeatureCollection;\n /** region id → { value, name } map for choropleth shading. */\n regionValues: Map<string, { value: number; name?: string }>;\n}\n\nfunction asLatLng(v: unknown): { lat: number; lng: number } | undefined {\n const c = v as { lat?: unknown; lng?: unknown } | undefined;\n return c && isFiniteNumber(c.lat) && isFiniteNumber(c.lng) ? { lat: c.lat, lng: c.lng } : undefined;\n}\n\nfunction normalizeMapData(data: MapData | MapMarker[] | undefined, config: Record<string, unknown>): NormalizedMapData {\n // The widget may be handed either the documented { markers, center, zoom } shape\n // or a bare marker array (parser output); narrow once so the rest is type-safe.\n const payload: MapData | undefined = Array.isArray(data) ? undefined : data;\n const rawMarkers: unknown = Array.isArray(data) ? data : data?.markers;\n const markers: MapMarker[] = Array.isArray(rawMarkers)\n ? (rawMarkers as MapMarker[]).filter((m) => m && isFiniteNumber(m.lat) && isFiniteNumber(m.lng))\n : [];\n\n // Choropleth inputs: GeoJSON (from data payload or config) + a region→value join.\n const geoJson = (payload?.geoJson ?? (config.geoJson as GeoJsonFeatureCollection | undefined)) || undefined;\n const regionIdField = (config.regionIdField as string) || 'id';\n const regionValues = new Map<string, { value: number; name?: string }>();\n const rawRegions: unknown = payload?.regions ?? config.regions;\n if (Array.isArray(rawRegions)) {\n for (const r of rawRegions as Array<Record<string, unknown>>) {\n if (!r) continue;\n const id = r[regionIdField] ?? r.id ?? r.region ?? r.code;\n const value = r.value ?? r.count ?? r.total;\n if (id != null && isFiniteNumber(value)) {\n regionValues.set(String(id), { value, name: typeof r.name === 'string' ? r.name : undefined });\n }\n }\n }\n\n // Data-payload center/zoom are an explicit intent and win over markers. Config\n // center/zoom (e.g. the WidgetRegistry default of {0,0}/2) are only a static\n // fallback for marker-less maps — otherwise that default would pin every marker\n // dataset to the world view instead of framing the markers.\n return {\n markers,\n center: asLatLng(payload?.center),\n zoom: isFiniteNumber(payload?.zoom) ? payload?.zoom : undefined,\n fallbackCenter: asLatLng(config.center),\n fallbackZoom: isFiniteNumber(config.zoom) ? (config.zoom as number) : undefined,\n geoJson: geoJson && Array.isArray(geoJson.features) ? geoJson : undefined,\n regionValues,\n };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MapWidget: FC<MapWidgetProps> = memo(function MapWidget({ widget, data, onClick, onDrilldown }) {\n const containerRef = useRef<HTMLDivElement>(null);\n const mapRef = useRef<L.Map | null>(null);\n // Signature of the last view we auto-framed to; lets us re-render markers on\n // every data change without snapping the viewport back when the framing\n // inputs (coords/center/zoom) are unchanged — e.g. a refetch of identical data.\n const lastFrameSigRef = useRef<string | null>(null);\n const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading');\n\n const config = useMemo(() => (widget?.config ?? {}) as Record<string, unknown>, [widget?.config]);\n const showMarkers = config.showMarkers !== false;\n // Heat-point density mode: render weighted, translucent circles whose overlap\n // accumulates into a heat-like surface (dependency-free — no leaflet.heat).\n const heatMode = config.mapMode === 'heat';\n const regionIdField = (config.regionIdField as string) || 'id';\n const { markers, center, zoom, fallbackCenter, fallbackZoom, geoJson, regionValues } = useMemo(\n () => normalizeMapData(data, config),\n [data, config]\n );\n const hasChoropleth = Boolean(geoJson && regionValues.size > 0);\n\n // Keep the latest click handler in a ref so marker bindings don't need rebinding.\n // Assign in an effect (not during render) per the React 19 useRef guidance.\n const clickRef = useRef<(marker: MapMarker) => void>(() => undefined);\n useEffect(() => {\n clickRef.current = (marker: MapMarker) => {\n if (onDrilldown) onDrilldown(marker as unknown as Record<string, unknown>);\n else if (onClick) onClick(marker);\n };\n }, [onClick, onDrilldown]);\n\n // Initialise the map once.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return undefined;\n ensureLeafletCss();\n\n let map: L.Map;\n try {\n map = L.map(el, { center: [20, 0], zoom: 2, scrollWheelZoom: true, attributionControl: true });\n const tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© OpenStreetMap contributors',\n maxZoom: 18,\n });\n // Leaflet reports tile failures asynchronously, so addTo() won't throw when\n // tiles are blocked/offline. Surface the error overlay only if a tile errors\n // before any tile has loaded (true offline/blocked), tolerating the odd\n // transient miss once the base map is up.\n let anyTileLoaded = false;\n tiles.on('tileload', () => {\n anyTileLoaded = true;\n // Recover if an early tile miss had shown the offline overlay — a flaky\n // connection that later succeeds shouldn't stay hidden behind it.\n setStatus((s) => (s === 'error' ? 'ready' : s));\n });\n tiles.on('tileerror', () => {\n if (!anyTileLoaded) setStatus('error');\n });\n tiles.addTo(map);\n mapRef.current = map;\n // Fresh map ⇒ force the next marker effect to (re)frame it. Without this the\n // frame-signature ref would survive a remount (e.g. React StrictMode's\n // double-invoke) and the new map would stay stuck at this init view.\n lastFrameSigRef.current = null;\n setStatus('ready');\n } catch {\n setStatus('error');\n return undefined;\n }\n\n // Widgets live in a resizable grid; keep tiles correct as the box changes.\n const ro =\n typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => mapRef.current?.invalidateSize()) : undefined;\n ro?.observe(el);\n const t = window.setTimeout(() => map.invalidateSize(), 80);\n\n return () => {\n window.clearTimeout(t);\n ro?.disconnect();\n map.remove();\n mapRef.current = null;\n };\n }, []);\n\n // Render markers + frame the view whenever the data changes.\n useEffect(() => {\n const map = mapRef.current;\n if (!map) return undefined;\n\n const layer = L.layerGroup();\n\n // Choropleth: shade GeoJSON regions by a single-hue intensity ramp (the\n // semantic `info` token at varying opacity) joined on the region id field.\n let geoLayer: L.GeoJSON | null = null;\n if (geoJson && regionValues.size > 0) {\n const vals = Array.from(regionValues.values()).map((v) => v.value);\n const min = Math.min(...vals);\n const max = Math.max(...vals);\n const span = max - min || 1;\n const FILL = 'var(--color-status-info, #3b82f6)';\n\n const lookup = (feature: { id?: string | number; properties?: Record<string, unknown> | null }) => {\n const props = feature.properties ?? {};\n const joinId = feature.id ?? props[regionIdField] ?? props.id ?? props.code;\n return joinId != null ? regionValues.get(String(joinId)) : undefined;\n };\n\n geoLayer = L.geoJSON(geoJson as unknown as Parameters<typeof L.geoJSON>[0], {\n style: (feature) => {\n const hit = feature ? lookup(feature) : undefined;\n const norm = hit ? (hit.value - min) / span : 0;\n return {\n fillColor: FILL,\n fillOpacity: hit ? 0.2 + norm * 0.65 : 0.05,\n color: 'var(--color-border-default, #cbd5e1)',\n weight: 1,\n };\n },\n onEachFeature: (feature, lyr) => {\n const hit = lookup(feature);\n const name =\n hit?.name ??\n (typeof feature.properties?.name === 'string' ? (feature.properties.name as string) : undefined) ??\n String(feature.id ?? '');\n const popupEl = document.createElement('div');\n const title = document.createElement('div');\n title.style.fontWeight = '600';\n title.textContent = name || 'Region';\n popupEl.appendChild(title);\n if (hit) {\n const val = document.createElement('div');\n val.textContent = hit.value.toLocaleString();\n popupEl.appendChild(val);\n }\n lyr.bindPopup(popupEl);\n lyr.on('click', () => clickRef.current({ name, lat: NaN, lng: NaN, value: hit?.value } as MapMarker));\n },\n });\n geoLayer.addTo(layer);\n }\n\n if (showMarkers && heatMode) {\n // Heat-point layer: translucent weighted circles, sized + colored by value.\n const weights = markers.map((mk) => (isFiniteNumber(mk.value) ? mk.value : 1));\n // reduce, not Math.min(...spread) — spreading a large array as args can\n // blow the call-stack (RangeError) for big datasets.\n const minW = weights.reduce((a, b) => Math.min(a, b), Infinity);\n const maxW = weights.reduce((a, b) => Math.max(a, b), -Infinity);\n const spanW = maxW - minW || 1;\n markers.forEach((marker, i) => {\n const norm = (weights[i] - minW) / spanW;\n const circle = L.circleMarker([marker.lat, marker.lng], {\n radius: 10 + norm * 22,\n fillColor: heatColor(norm),\n // Both radius and opacity ramp with intensity for a truer density look.\n fillOpacity: 0.25 + norm * 0.35,\n stroke: false,\n }).addTo(layer);\n circle.bindPopup(buildMarkerPopup(marker));\n circle.on('click', () => clickRef.current(marker));\n });\n } else if (showMarkers) {\n for (const marker of markers) {\n // Build the pin as a DOM element (not an HTML string) so the marker colour\n // is assigned via the style API and can never be interpreted as markup —\n // defense-in-depth alongside safeColor().\n const pin = document.createElement('div');\n Object.assign(pin.style, {\n width: '14px',\n height: '14px',\n borderRadius: '50% 50% 50% 0',\n transform: 'rotate(-45deg)',\n background: safeColor(marker.color),\n border: '2px solid #fff',\n boxShadow: '0 1px 4px rgba(0,0,0,.4)',\n });\n const icon = L.divIcon({\n className: 'bc-map-marker',\n html: pin,\n iconSize: [14, 14],\n iconAnchor: [7, 14],\n });\n const m = L.marker([marker.lat, marker.lng], { icon }).addTo(layer);\n m.bindPopup(buildMarkerPopup(marker));\n m.on('click', () => clickRef.current(marker));\n }\n }\n layer.addTo(map);\n\n // Only (re)frame the view when the framing inputs actually change — otherwise\n // a data-prop reference change (e.g. a refetch returning identical markers)\n // would discard the user's current pan/zoom.\n const frameSig = JSON.stringify({\n m: markers.map((mk) => [mk.lat, mk.lng]),\n c: center ? [center.lat, center.lng] : null,\n z: zoom ?? null,\n fc: fallbackCenter ? [fallbackCenter.lat, fallbackCenter.lng] : null,\n fz: fallbackZoom ?? null,\n g: geoJson ? regionValues.size : 0,\n });\n if (frameSig !== lastFrameSigRef.current) {\n lastFrameSigRef.current = frameSig;\n // Framing priority: data-payload center → frame the markers → frame the\n // choropleth regions → config fallback center → zoom only → world view.\n // Markers/regions win over the config default so a palette-created map frames\n // its data instead of {0,0}.\n if (center) {\n map.setView([center.lat, center.lng], zoom ?? map.getZoom());\n } else if (!markers.length && geoLayer) {\n const b = geoLayer.getBounds();\n if (b.isValid()) map.fitBounds(b, { padding: [24, 24], maxZoom: 12 });\n } else if (markers.length === 1) {\n map.setView([markers[0].lat, markers[0].lng], zoom ?? 8);\n } else if (markers.length > 1) {\n const bounds = L.latLngBounds(markers.map((m) => [m.lat, m.lng] as [number, number]));\n map.fitBounds(bounds, { padding: [32, 32], maxZoom: 12 });\n } else if (fallbackCenter) {\n map.setView([fallbackCenter.lat, fallbackCenter.lng], zoom ?? fallbackZoom ?? map.getZoom());\n } else if (isFiniteNumber(zoom ?? fallbackZoom)) {\n map.setZoom((zoom ?? fallbackZoom) as number);\n }\n }\n\n const resize = window.setTimeout(() => map.invalidateSize(), 0);\n\n return () => {\n window.clearTimeout(resize);\n layer.remove();\n };\n }, [\n markers,\n center,\n zoom,\n fallbackCenter,\n fallbackZoom,\n showMarkers,\n heatMode,\n geoJson,\n regionValues,\n regionIdField,\n ]);\n\n return (\n <div className=\"relative h-full w-full overflow-hidden rounded-lg bg-bg-sunken\">\n <div ref={containerRef} className=\"h-full w-full\" />\n\n {status !== 'error' && markers.length === 0 && !hasChoropleth && (\n <div className=\"pointer-events-none absolute inset-0 z-[500] flex items-center justify-center bg-bg-sunken/70 text-sm text-text-secondary\">\n <div className=\"text-center\">\n <svg\n className=\"mx-auto mb-2 h-12 w-12 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"\n />\n </svg>\n <p>No map data</p>\n </div>\n </div>\n )}\n\n {status === 'error' && (\n <div className=\"absolute inset-0 z-[500] flex items-center justify-center bg-bg-sunken px-4 text-center text-sm text-text-secondary\">\n The map needs an internet connection and couldn’t load.\n </div>\n )}\n </div>\n );\n});\n\nexport default MapWidget;\n"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ var i = t(function({ value: t, label: i, positiveIsGood: a = !0, size: o = "md"
|
|
|
9
9
|
inline-flex items-center gap-1
|
|
10
10
|
rounded-full font-medium
|
|
11
11
|
${o === "sm" ? "text-xs px-1.5 py-0.5" : "text-sm px-2 py-1"}
|
|
12
|
-
${c === null ? "bg-bg-
|
|
12
|
+
${c === null ? "bg-bg-sunken text-text-secondary" : c ? "bg-state-success/10 text-state-success" : "bg-state-error/10 text-state-error"}
|
|
13
13
|
`,
|
|
14
14
|
children: [
|
|
15
15
|
/* @__PURE__ */ n(e, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComparisonBadge.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/ComparisonBadge.tsx"],"sourcesContent":["/**\n * ComparisonBadge Component\n *\n * Displays percentage change with color coding.\n */\n\nimport { type FC, memo } from 'react';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\n\nexport interface ComparisonBadgeProps {\n /** Percentage change value */\n value: number;\n /** Label for the comparison (e.g., \"vs last week\") */\n label?: string;\n /** Whether positive change is good */\n positiveIsGood?: boolean;\n /** Size variant */\n size?: 'sm' | 'md';\n}\n\nexport const ComparisonBadge: FC<ComparisonBadgeProps> = memo(function ComparisonBadge({\n value,\n label,\n positiveIsGood = true,\n size = 'md',\n}) {\n // Determine trend direction\n const direction: TrendDirection = value > 0 ? 'up' : value < 0 ? 'down' : 'flat';\n\n // Determine if the change is good\n const isGood = direction === 'flat' ? null : positiveIsGood ? direction === 'up' : direction === 'down';\n\n // Get color classes\n const getColorClasses = (): string => {\n if (isGood === null) {\n return 'bg-bg-
|
|
1
|
+
{"version":3,"file":"ComparisonBadge.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/ComparisonBadge.tsx"],"sourcesContent":["/**\n * ComparisonBadge Component\n *\n * Displays percentage change with color coding.\n */\n\nimport { type FC, memo } from 'react';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\n\nexport interface ComparisonBadgeProps {\n /** Percentage change value */\n value: number;\n /** Label for the comparison (e.g., \"vs last week\") */\n label?: string;\n /** Whether positive change is good */\n positiveIsGood?: boolean;\n /** Size variant */\n size?: 'sm' | 'md';\n}\n\nexport const ComparisonBadge: FC<ComparisonBadgeProps> = memo(function ComparisonBadge({\n value,\n label,\n positiveIsGood = true,\n size = 'md',\n}) {\n // Determine trend direction\n const direction: TrendDirection = value > 0 ? 'up' : value < 0 ? 'down' : 'flat';\n\n // Determine if the change is good\n const isGood = direction === 'flat' ? null : positiveIsGood ? direction === 'up' : direction === 'down';\n\n // Get color classes\n const getColorClasses = (): string => {\n if (isGood === null) {\n return 'bg-bg-sunken text-text-secondary';\n }\n return isGood ? 'bg-state-success/10 text-state-success' : 'bg-state-error/10 text-state-error';\n };\n\n // Format percentage\n const formatPercentage = (): string => {\n const absValue = Math.abs(value);\n const formatted = absValue >= 100 ? absValue.toFixed(0) : absValue.toFixed(1);\n const sign = value > 0 ? '+' : value < 0 ? '-' : '';\n return `${sign}${formatted}%`;\n };\n\n const sizeClasses = size === 'sm' ? 'text-xs px-1.5 py-0.5' : 'text-sm px-2 py-1';\n const colorClasses = getColorClasses();\n\n return (\n <div\n className={`\n inline-flex items-center gap-1\n rounded-full font-medium\n ${sizeClasses}\n ${colorClasses}\n `}\n >\n <TrendIndicator direction={direction} size={size === 'sm' ? 'sm' : 'md'} positiveIsGood={positiveIsGood} />\n <span>{formatPercentage()}</span>\n {label && <span className=\"text-text-secondary font-normal\">{label}</span>}\n </div>\n );\n});\n\nexport default ComparisonBadge;\n"],"mappings":";;;;AAoBA,IAAa,IAA4C,EAAK,SAAyB,EACrF,UACA,UACA,oBAAiB,IACjB,UAAO,QACN;CAED,IAAM,IAA4B,IAAQ,IAAI,OAAO,IAAQ,IAAI,SAAS,QAGpE,IAAS,MAAc,SAAS,OAAO,IAAiB,MAAc,OAAO,MAAc;AAqBjG,QACE,kBAAC,OAAD;EACE,WAAW;;;YALK,MAAS,OAAO,0BAA0B,oBAQxC;YAtBhB,MAAW,OACN,qCAEF,IAAS,2CAA2C,qCAoBtC;;YALrB;GAQE,kBAAC,GAAD;IAA2B;IAAW,MAAM,MAAS,OAAO,OAAO;IAAsB;IAAkB,CAAA;GAC3G,kBAAC,QAAD,EAAA,iBApBmC;IACrC,IAAM,IAAW,KAAK,IAAI,EAAM,EAC1B,IAAY,KAAY,MAAM,EAAS,QAAQ,EAAE,GAAG,EAAS,QAAQ,EAAE;AAE7E,WAAO,GADM,IAAQ,IAAI,MAAM,IAAQ,IAAI,MAAM,KAChC,EAAU;OAgBA,EAAQ,CAAA;GAChC,KAAS,kBAAC,QAAD;IAAM,WAAU;cAAmC;IAAa,CAAA;GACtE;;EAER"}
|
|
@@ -112,7 +112,7 @@ var g = c(function({ widget: c, data: g, onClick: _, onDrilldown: v }) {
|
|
|
112
112
|
return /* @__PURE__ */ f("div", {
|
|
113
113
|
className: `
|
|
114
114
|
flex flex-col h-full p-4
|
|
115
|
-
${D ? "cursor-pointer hover:bg-bg-
|
|
115
|
+
${D ? "cursor-pointer hover:bg-bg-sunken/50 transition-colors" : ""}
|
|
116
116
|
`,
|
|
117
117
|
onClick: E,
|
|
118
118
|
role: D ? "button" : void 0,
|
|
@@ -152,7 +152,7 @@ var g = c(function({ widget: c, data: g, onClick: _, onDrilldown: v }) {
|
|
|
152
152
|
className: "flex justify-between text-xs text-text-secondary mb-1",
|
|
153
153
|
children: [/* @__PURE__ */ f("span", { children: ["Target: ", p(b.target, y)] }), /* @__PURE__ */ f("span", { children: [(b.value / b.target * 100).toFixed(0), "%"] })]
|
|
154
154
|
}), /* @__PURE__ */ d("div", {
|
|
155
|
-
className: "h-1.5 bg-bg-
|
|
155
|
+
className: "h-1.5 bg-bg-sunken rounded-full overflow-hidden",
|
|
156
156
|
children: /* @__PURE__ */ d("div", {
|
|
157
157
|
className: `h-full rounded-full transition-all ${b.value >= b.target ? "bg-state-success" : "bg-action-primary-bg"}`,
|
|
158
158
|
style: { width: `${Math.min(b.value / b.target * 100, 100)}%` }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MetricCardWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx"],"sourcesContent":["/**\n * MetricCardWidget Component\n *\n * Displays a single KPI/metric with trend, comparison, and optional sparkline.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, MetricCardConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\nimport { ComparisonBadge } from './ComparisonBadge';\nimport { SparklineChart, type SparklineDataPoint } from './SparklineChart';\nimport { firstObjectRow, inferValueField, unwrapRows } from '../utils/inferFields';\nimport { resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MetricCardWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Fetched data */\n data?: MetricCardData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives metric data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MetricCardData {\n /** Primary value to display */\n value: number | string;\n /** Previous value for comparison */\n previousValue?: number;\n /** Percentage change from previous */\n changePercent?: number;\n /** Trend direction override */\n trend?: TrendDirection;\n /** Sparkline data points */\n sparklineData?: SparklineDataPoint[];\n /** Target value for comparison */\n target?: number;\n /** Unit label (e.g., \"users\", \"$\") */\n unit?: string;\n /** Whether the metric is a currency */\n isCurrency?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Format a number based on configuration\n */\nfunction formatValue(value: number | string, config?: MetricCardConfig): string {\n if (typeof value === 'string') return value;\n\n const { format = 'number', prefix, suffix } = config || {};\n\n let formatted: string;\n\n switch (format) {\n case 'currency':\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(value);\n break;\n\n case 'percentage':\n formatted = `${(value * 100).toFixed(1)}%`;\n break;\n\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n break;\n\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n\n // Apply prefix/suffix if provided\n if (prefix) formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n\n return formatted;\n}\n\n/**\n * Calculate trend direction from change percent\n */\nfunction calculateTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n/**\n * Get sparkline color based on trend\n */\nfunction getSparklineColor(trend: TrendDirection, positiveIsGood: boolean): 'success' | 'error' | 'muted' {\n if (trend === 'flat') return 'muted';\n const isGood = positiveIsGood ? trend === 'up' : trend === 'down';\n return isGood ? 'success' : 'error';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MetricCardWidget: FC<MetricCardWidgetProps> = memo(function MetricCardWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as MetricCardConfig | undefined;\n\n // Normalize data - extract value from object if valueField is configured\n const data = useMemo((): MetricCardData | undefined => {\n if (!rawData) return undefined;\n\n // Helper to get nested value using dot notation (e.g., \"summary.totalSales\")\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {\n return path.split('.').reduce((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj as unknown);\n };\n\n // If data already has 'value' property, use it directly\n if ('value' in rawData && rawData.value !== undefined) {\n return rawData as MetricCardData;\n }\n\n // If valueField is configured, extract the value from that field (supports dot notation)\n const valueField = (config as unknown as Record<string, unknown> | undefined)?.valueField as string | undefined;\n if (valueField && typeof rawData === 'object') {\n const extractedValue = getNestedValue(rawData as unknown as Record<string, unknown>, valueField);\n if (extractedValue !== undefined) {\n // Also try to extract previousValue if previousValueField is configured\n const previousValueField = (config as unknown as Record<string, unknown> | undefined)?.previousValueField as\n | string\n | undefined;\n const previousValue = previousValueField\n ? (getNestedValue(rawData as unknown as Record<string, unknown>, previousValueField) as number | undefined)\n : undefined;\n\n return {\n value: extractedValue as number | string,\n previousValue,\n changePercent:\n previousValue !== undefined && typeof extractedValue === 'number'\n ? ((extractedValue - previousValue) / previousValue) * 100\n : undefined,\n };\n }\n }\n\n // Try common field names at top level\n const commonFields = ['value', 'total', 'amount', 'count', 'sum', 'totalSales', 'totalRevenue', 'totalOrders'];\n for (const field of commonFields) {\n if (typeof rawData === 'object' && field in rawData) {\n const val = (rawData as unknown as Record<string, unknown>)[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n // Try looking in nested 'summary' object (common pattern)\n if (typeof rawData === 'object' && 'summary' in rawData) {\n const summary = (rawData as Record<string, unknown>).summary as Record<string, unknown> | undefined;\n if (summary && typeof summary === 'object') {\n for (const field of commonFields) {\n if (field in summary) {\n const val = summary[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n }\n }\n\n // If rawData is a number or string directly, use it\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // BOFF-2246 smart default: if rawData is an array (or envelope around an\n // array) with object rows, use the configured valueField if it resolves\n // on any row (supports dotted paths like \"metrics.revenue\") — otherwise\n // pick the first numeric field from row[0]. This path only fires when\n // the earlier top-level extraction didn't find anything, but an explicit\n // `config.valueField` (flat or dotted) must always win over inference.\n const firstRow = firstObjectRow(rawData);\n if (firstRow) {\n const configuredField = (config as unknown as Record<string, unknown> | undefined)?.valueField as\n | string\n | undefined;\n if (configuredField) {\n // Configured field is authoritative. Scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column\n // from row[0] (that would silently misrepresent the user's mapping).\n const rows = unwrapRows(rawData) ?? [firstRow];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolved = getNestedValue(r as Record<string, unknown>, configuredField);\n if (typeof resolved === 'number' || typeof resolved === 'string') {\n return { value: resolved };\n }\n }\n }\n return undefined;\n }\n const inferredField = inferValueField(firstRow);\n if (inferredField) {\n const val = firstRow[inferredField];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n return undefined;\n }, [rawData, config]);\n\n // Calculate derived values\n const trend = useMemo(() => {\n return data?.trend || calculateTrend(data?.changePercent);\n }, [data?.trend, data?.changePercent]);\n\n const positiveIsGood = config?.positiveIsGood ?? true;\n\n const formattedValue = useMemo(() => {\n // Treat 0 / \"\" as real values — only a missing value renders the em dash.\n // (`if (!data?.value)` would wrongly blank out a legitimate 0, e.g. an ED\n // boarding-compliance of 0% or an available-bed count of 0.)\n if (data?.value === undefined || data?.value === null) return '—';\n return formatValue(data.value, config);\n }, [data?.value, config]);\n\n const sparklineColor = useMemo(() => {\n return getSparklineColor(trend, positiveIsGood);\n }, [trend, positiveIsGood]);\n\n // Conditional formatting on the metric value (threshold-driven color, not\n // just trend direction). Falls back to the default primary text color.\n const valueConditionalClass = useMemo(() => {\n const style = resolveConditionalStyle(data?.value, config?.conditionalRules);\n return conditionalStyleToClassName(style);\n }, [data?.value, config?.conditionalRules]);\n\n // Handle click\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown) {\n onDrilldown({\n value: data?.value,\n changePercent: data?.changePercent,\n trend,\n target: data?.target,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, trend]);\n\n // Determine if clickable\n const isClickable = Boolean(onDrilldown || (onClick && widget.drilldowns?.length));\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-muted/50 transition-colors' : ''}\n `}\n onClick={handleClick}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n onKeyDown={(e) => {\n if (isClickable && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n handleClick();\n }\n }}\n >\n {/* Subtitle/Label */}\n {config?.subtitle && <p className=\"text-xs text-text-secondary mb-1 truncate\">{config.subtitle}</p>}\n\n {/* Main Value */}\n <div className=\"flex items-baseline gap-2 mb-2\">\n <span\n className={`text-3xl font-semibold tracking-tight ${valueConditionalClass || 'text-text-primary'}`}\n title={String(data?.value)}\n >\n {formattedValue}\n </span>\n\n {/* Trend Indicator (inline) */}\n {config?.showTrend !== false && data?.changePercent !== undefined && (\n <TrendIndicator direction={trend} size=\"md\" positiveIsGood={positiveIsGood} />\n )}\n </div>\n\n {/* Comparison Badge */}\n {config?.showComparison !== false && data?.changePercent !== undefined && (\n <div className=\"mb-3\">\n <ComparisonBadge\n value={data.changePercent}\n label={config?.comparisonLabel}\n positiveIsGood={positiveIsGood}\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Target Progress */}\n {data?.target !== undefined && typeof data.value === 'number' && (\n <div className=\"mb-3\">\n <div className=\"flex justify-between text-xs text-text-secondary mb-1\">\n <span>Target: {formatValue(data.target, config)}</span>\n <span>{((data.value / data.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1.5 bg-bg-muted rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${\n data.value >= data.target ? 'bg-state-success' : 'bg-action-primary-bg'\n }`}\n style={{\n width: `${Math.min((data.value / data.target) * 100, 100)}%`,\n }}\n />\n </div>\n </div>\n )}\n\n {/* Sparkline */}\n {config?.showSparkline !== false && data?.sparklineData?.length ? (\n <div className=\"mt-auto pt-2\">\n <SparklineChart data={data.sparklineData} width={120} height={32} color={sparklineColor} showFill />\n </div>\n ) : null}\n\n {/* Drilldown Indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-2 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default MetricCardWidget;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MetricCardWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/MetricCardWidget.tsx"],"sourcesContent":["/**\n * MetricCardWidget Component\n *\n * Displays a single KPI/metric with trend, comparison, and optional sparkline.\n */\n\nimport { type FC, memo, useMemo, useCallback } from 'react';\nimport type { Widget, MetricCardConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from './TrendIndicator';\nimport { ComparisonBadge } from './ComparisonBadge';\nimport { SparklineChart, type SparklineDataPoint } from './SparklineChart';\nimport { firstObjectRow, inferValueField, unwrapRows } from '../utils/inferFields';\nimport { resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface MetricCardWidgetProps {\n /** Widget configuration */\n widget: Widget;\n /** Fetched data */\n data?: MetricCardData;\n /** Click handler for drilldown */\n onClick?: () => void;\n /** Drilldown handler - receives metric data */\n onDrilldown?: (selectedData: Record<string, unknown>) => void;\n}\n\nexport interface MetricCardData {\n /** Primary value to display */\n value: number | string;\n /** Previous value for comparison */\n previousValue?: number;\n /** Percentage change from previous */\n changePercent?: number;\n /** Trend direction override */\n trend?: TrendDirection;\n /** Sparkline data points */\n sparklineData?: SparklineDataPoint[];\n /** Target value for comparison */\n target?: number;\n /** Unit label (e.g., \"users\", \"$\") */\n unit?: string;\n /** Whether the metric is a currency */\n isCurrency?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Format a number based on configuration\n */\nfunction formatValue(value: number | string, config?: MetricCardConfig): string {\n if (typeof value === 'string') return value;\n\n const { format = 'number', prefix, suffix } = config || {};\n\n let formatted: string;\n\n switch (format) {\n case 'currency':\n formatted = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n minimumFractionDigits: 0,\n maximumFractionDigits: 2,\n }).format(value);\n break;\n\n case 'percentage':\n formatted = `${(value * 100).toFixed(1)}%`;\n break;\n\n case 'compact':\n formatted = new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n break;\n\n case 'number':\n default:\n formatted = new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n\n // Apply prefix/suffix if provided\n if (prefix) formatted = `${prefix}${formatted}`;\n if (suffix) formatted = `${formatted}${suffix}`;\n\n return formatted;\n}\n\n/**\n * Calculate trend direction from change percent\n */\nfunction calculateTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n/**\n * Get sparkline color based on trend\n */\nfunction getSparklineColor(trend: TrendDirection, positiveIsGood: boolean): 'success' | 'error' | 'muted' {\n if (trend === 'flat') return 'muted';\n const isGood = positiveIsGood ? trend === 'up' : trend === 'down';\n return isGood ? 'success' : 'error';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const MetricCardWidget: FC<MetricCardWidgetProps> = memo(function MetricCardWidget({\n widget,\n data: rawData,\n onClick,\n onDrilldown,\n}) {\n const config = widget.config as unknown as MetricCardConfig | undefined;\n\n // Normalize data - extract value from object if valueField is configured\n const data = useMemo((): MetricCardData | undefined => {\n if (!rawData) return undefined;\n\n // Helper to get nested value using dot notation (e.g., \"summary.totalSales\")\n const getNestedValue = (obj: Record<string, unknown>, path: string): unknown => {\n return path.split('.').reduce((current, key) => {\n return current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined;\n }, obj as unknown);\n };\n\n // If data already has 'value' property, use it directly\n if ('value' in rawData && rawData.value !== undefined) {\n return rawData as MetricCardData;\n }\n\n // If valueField is configured, extract the value from that field (supports dot notation)\n const valueField = (config as unknown as Record<string, unknown> | undefined)?.valueField as string | undefined;\n if (valueField && typeof rawData === 'object') {\n const extractedValue = getNestedValue(rawData as unknown as Record<string, unknown>, valueField);\n if (extractedValue !== undefined) {\n // Also try to extract previousValue if previousValueField is configured\n const previousValueField = (config as unknown as Record<string, unknown> | undefined)?.previousValueField as\n | string\n | undefined;\n const previousValue = previousValueField\n ? (getNestedValue(rawData as unknown as Record<string, unknown>, previousValueField) as number | undefined)\n : undefined;\n\n return {\n value: extractedValue as number | string,\n previousValue,\n changePercent:\n previousValue !== undefined && typeof extractedValue === 'number'\n ? ((extractedValue - previousValue) / previousValue) * 100\n : undefined,\n };\n }\n }\n\n // Try common field names at top level\n const commonFields = ['value', 'total', 'amount', 'count', 'sum', 'totalSales', 'totalRevenue', 'totalOrders'];\n for (const field of commonFields) {\n if (typeof rawData === 'object' && field in rawData) {\n const val = (rawData as unknown as Record<string, unknown>)[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n // Try looking in nested 'summary' object (common pattern)\n if (typeof rawData === 'object' && 'summary' in rawData) {\n const summary = (rawData as Record<string, unknown>).summary as Record<string, unknown> | undefined;\n if (summary && typeof summary === 'object') {\n for (const field of commonFields) {\n if (field in summary) {\n const val = summary[field];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n }\n }\n\n // If rawData is a number or string directly, use it\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // BOFF-2246 smart default: if rawData is an array (or envelope around an\n // array) with object rows, use the configured valueField if it resolves\n // on any row (supports dotted paths like \"metrics.revenue\") — otherwise\n // pick the first numeric field from row[0]. This path only fires when\n // the earlier top-level extraction didn't find anything, but an explicit\n // `config.valueField` (flat or dotted) must always win over inference.\n const firstRow = firstObjectRow(rawData);\n if (firstRow) {\n const configuredField = (config as unknown as Record<string, unknown> | undefined)?.valueField as\n | string\n | undefined;\n if (configuredField) {\n // Configured field is authoritative. Scan every row for it so sparse\n // envelopes like `{data:[{id:1},{revenue:120}]}` with\n // `valueField:\"revenue\"` still resolve. If no row carries it, return\n // undefined rather than substituting an unrelated numeric column\n // from row[0] (that would silently misrepresent the user's mapping).\n const rows = unwrapRows(rawData) ?? [firstRow];\n for (const r of rows) {\n if (r && typeof r === 'object') {\n const resolved = getNestedValue(r as Record<string, unknown>, configuredField);\n if (typeof resolved === 'number' || typeof resolved === 'string') {\n return { value: resolved };\n }\n }\n }\n return undefined;\n }\n const inferredField = inferValueField(firstRow);\n if (inferredField) {\n const val = firstRow[inferredField];\n if (typeof val === 'number' || typeof val === 'string') {\n return { value: val };\n }\n }\n }\n\n return undefined;\n }, [rawData, config]);\n\n // Calculate derived values\n const trend = useMemo(() => {\n return data?.trend || calculateTrend(data?.changePercent);\n }, [data?.trend, data?.changePercent]);\n\n const positiveIsGood = config?.positiveIsGood ?? true;\n\n const formattedValue = useMemo(() => {\n // Treat 0 / \"\" as real values — only a missing value renders the em dash.\n // (`if (!data?.value)` would wrongly blank out a legitimate 0, e.g. an ED\n // boarding-compliance of 0% or an available-bed count of 0.)\n if (data?.value === undefined || data?.value === null) return '—';\n return formatValue(data.value, config);\n }, [data?.value, config]);\n\n const sparklineColor = useMemo(() => {\n return getSparklineColor(trend, positiveIsGood);\n }, [trend, positiveIsGood]);\n\n // Conditional formatting on the metric value (threshold-driven color, not\n // just trend direction). Falls back to the default primary text color.\n const valueConditionalClass = useMemo(() => {\n const style = resolveConditionalStyle(data?.value, config?.conditionalRules);\n return conditionalStyleToClassName(style);\n }, [data?.value, config?.conditionalRules]);\n\n // Handle click\n const handleClick = useCallback(() => {\n // Support both legacy onClick and new onDrilldown\n if (onDrilldown) {\n onDrilldown({\n value: data?.value,\n changePercent: data?.changePercent,\n trend,\n target: data?.target,\n });\n } else if (onClick && widget.drilldowns?.length) {\n onClick();\n }\n }, [onClick, onDrilldown, widget.drilldowns, data, trend]);\n\n // Determine if clickable\n const isClickable = Boolean(onDrilldown || (onClick && widget.drilldowns?.length));\n\n return (\n <div\n className={`\n flex flex-col h-full p-4\n ${isClickable ? 'cursor-pointer hover:bg-bg-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 {/* Subtitle/Label */}\n {config?.subtitle && <p className=\"text-xs text-text-secondary mb-1 truncate\">{config.subtitle}</p>}\n\n {/* Main Value */}\n <div className=\"flex items-baseline gap-2 mb-2\">\n <span\n className={`text-3xl font-semibold tracking-tight ${valueConditionalClass || 'text-text-primary'}`}\n title={String(data?.value)}\n >\n {formattedValue}\n </span>\n\n {/* Trend Indicator (inline) */}\n {config?.showTrend !== false && data?.changePercent !== undefined && (\n <TrendIndicator direction={trend} size=\"md\" positiveIsGood={positiveIsGood} />\n )}\n </div>\n\n {/* Comparison Badge */}\n {config?.showComparison !== false && data?.changePercent !== undefined && (\n <div className=\"mb-3\">\n <ComparisonBadge\n value={data.changePercent}\n label={config?.comparisonLabel}\n positiveIsGood={positiveIsGood}\n size=\"sm\"\n />\n </div>\n )}\n\n {/* Target Progress */}\n {data?.target !== undefined && typeof data.value === 'number' && (\n <div className=\"mb-3\">\n <div className=\"flex justify-between text-xs text-text-secondary mb-1\">\n <span>Target: {formatValue(data.target, config)}</span>\n <span>{((data.value / data.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1.5 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full transition-all ${\n data.value >= data.target ? 'bg-state-success' : 'bg-action-primary-bg'\n }`}\n style={{\n width: `${Math.min((data.value / data.target) * 100, 100)}%`,\n }}\n />\n </div>\n </div>\n )}\n\n {/* Sparkline */}\n {config?.showSparkline !== false && data?.sparklineData?.length ? (\n <div className=\"mt-auto pt-2\">\n <SparklineChart data={data.sparklineData} width={120} height={32} color={sparklineColor} showFill />\n </div>\n ) : null}\n\n {/* Drilldown Indicator */}\n {isClickable && (\n <div className=\"mt-auto pt-2 flex items-center justify-end\">\n <span className=\"text-xs text-text-secondary flex items-center gap-1\">\n View details\n <svg className=\"w-3 h-3\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5l7 7-7 7\" />\n </svg>\n </span>\n </div>\n )}\n </div>\n );\n});\n\nexport default MetricCardWidget;\n"],"mappings":"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"}
|
|
@@ -212,7 +212,7 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
212
212
|
if (r > 0) {
|
|
213
213
|
let l = g(p, D, O, t);
|
|
214
214
|
i.push(/* @__PURE__ */ d("tr", {
|
|
215
|
-
className: "hover:bg-bg-
|
|
215
|
+
className: "hover:bg-bg-sunken/50 transition-colors",
|
|
216
216
|
children: [
|
|
217
217
|
/* @__PURE__ */ u("td", {
|
|
218
218
|
className: "sticky left-0 bg-bg-surface border-r border-border-default px-3 py-2 font-medium text-sm",
|
|
@@ -223,7 +223,7 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
223
223
|
_.expandable && a && /* @__PURE__ */ u("button", {
|
|
224
224
|
type: "button",
|
|
225
225
|
onClick: () => V(n),
|
|
226
|
-
className: "p-0.5 hover:bg-bg-
|
|
226
|
+
className: "p-0.5 hover:bg-bg-sunken rounded",
|
|
227
227
|
children: u(c ? o : s, { className: "w-4 h-4 text-text-secondary" })
|
|
228
228
|
}),
|
|
229
229
|
!a && _.expandable && /* @__PURE__ */ u("span", { className: "w-5" }),
|
|
@@ -240,7 +240,7 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
240
240
|
}, `${e}-${r.field}`));
|
|
241
241
|
}),
|
|
242
242
|
_.showRowTotals && O.map((t) => /* @__PURE__ */ u("td", {
|
|
243
|
-
className: "px-3 py-2 text-sm text-right tabular-nums font-medium bg-bg-
|
|
243
|
+
className: "px-3 py-2 text-sm text-right tabular-nums font-medium bg-bg-sunken/30",
|
|
244
244
|
children: B(e.values[t.field] || 0)
|
|
245
245
|
}, `total-${t.field}`))
|
|
246
246
|
]
|
|
@@ -299,10 +299,10 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
299
299
|
/* @__PURE__ */ d("thead", {
|
|
300
300
|
className: "sticky top-0 z-10",
|
|
301
301
|
children: [/* @__PURE__ */ d("tr", {
|
|
302
|
-
className: "bg-bg-
|
|
302
|
+
className: "bg-bg-sunken",
|
|
303
303
|
children: [
|
|
304
304
|
/* @__PURE__ */ u("th", {
|
|
305
|
-
className: "sticky left-0 bg-bg-
|
|
305
|
+
className: "sticky left-0 bg-bg-sunken border-r border-b border-border-default px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-text-secondary",
|
|
306
306
|
children: E.length > 0 ? E.map((e) => e.replace(/_/g, " ")).join(" / ") : "Values"
|
|
307
307
|
}),
|
|
308
308
|
P.map((e) => /* @__PURE__ */ u("th", {
|
|
@@ -312,7 +312,7 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
312
312
|
}, e)),
|
|
313
313
|
_.showRowTotals && /* @__PURE__ */ u("th", {
|
|
314
314
|
colSpan: O.length || 1,
|
|
315
|
-
className: "border-b border-border-default px-3 py-2 text-center text-xs font-semibold uppercase tracking-wider text-text-secondary bg-bg-
|
|
315
|
+
className: "border-b border-border-default px-3 py-2 text-center text-xs font-semibold uppercase tracking-wider text-text-secondary bg-bg-sunken/50",
|
|
316
316
|
children: "Total"
|
|
317
317
|
})
|
|
318
318
|
]
|
|
@@ -325,7 +325,7 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
325
325
|
children: t.label || t.field.replace(/_/g, " ")
|
|
326
326
|
}, `${e}-${t.field}`))),
|
|
327
327
|
_.showRowTotals && O.map((e) => /* @__PURE__ */ u("th", {
|
|
328
|
-
className: "border-b border-border-default px-3 py-1 text-center text-xs text-text-secondary bg-bg-
|
|
328
|
+
className: "border-b border-border-default px-3 py-1 text-center text-xs text-text-secondary bg-bg-sunken/30",
|
|
329
329
|
children: e.label || e.field.replace(/_/g, " ")
|
|
330
330
|
}, `total-${e.field}`))
|
|
331
331
|
]
|
|
@@ -336,10 +336,10 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
336
336
|
children: U(N, {}, "root", 0)
|
|
337
337
|
}),
|
|
338
338
|
_.showColumnTotals && /* @__PURE__ */ u("tfoot", { children: /* @__PURE__ */ d("tr", {
|
|
339
|
-
className: "bg-bg-
|
|
339
|
+
className: "bg-bg-sunken/50 font-medium",
|
|
340
340
|
children: [
|
|
341
341
|
/* @__PURE__ */ u("td", {
|
|
342
|
-
className: "sticky left-0 bg-bg-
|
|
342
|
+
className: "sticky left-0 bg-bg-sunken/50 border-r border-t border-border-default px-3 py-2 text-sm",
|
|
343
343
|
children: "Total"
|
|
344
344
|
}),
|
|
345
345
|
P.map((e) => {
|
|
@@ -350,7 +350,7 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
350
350
|
}, `footer-${e}-${n.field}`));
|
|
351
351
|
}),
|
|
352
352
|
_.showRowTotals && _.showGrandTotal && O.map((e) => /* @__PURE__ */ u("td", {
|
|
353
|
-
className: "border-t border-border-default px-3 py-2 text-sm text-right tabular-nums font-bold bg-bg-
|
|
353
|
+
className: "border-t border-border-default px-3 py-2 text-sm text-right tabular-nums font-bold bg-bg-sunken",
|
|
354
354
|
children: B(N.values[e.field] || 0)
|
|
355
355
|
}, `grand-${e.field}`))
|
|
356
356
|
]
|
|
@@ -401,7 +401,7 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
401
401
|
onChange: (t) => o(e, t.target.value),
|
|
402
402
|
onClick: (e) => e.stopPropagation(),
|
|
403
403
|
"aria-label": `Aggregation for ${e}`,
|
|
404
|
-
className: "ml-1 bg-bg-
|
|
404
|
+
className: "ml-1 bg-bg-sunken text-text-secondary rounded px-1 py-0.5 text-[10px] uppercase",
|
|
405
405
|
children: f.map((e) => /* @__PURE__ */ u("option", {
|
|
406
406
|
value: e,
|
|
407
407
|
children: e
|
|
@@ -414,7 +414,7 @@ var _ = n(function({ widget: n, data: c = [], onDrilldown: f }) {
|
|
|
414
414
|
},
|
|
415
415
|
onDragLeave: () => l((e) => e === t ? null : e),
|
|
416
416
|
onDrop: p(t),
|
|
417
|
-
className: `flex-1 min-w-[8rem] rounded-md border p-2 transition-colors ${s === t ? "border-action-primary-bg bg-action-primary-bg/5" : "border-border-default bg-bg-
|
|
417
|
+
className: `flex-1 min-w-[8rem] rounded-md border p-2 transition-colors ${s === t ? "border-action-primary-bg bg-action-primary-bg/5" : "border-border-default bg-bg-sunken/30"}`,
|
|
418
418
|
children: [/* @__PURE__ */ u("div", {
|
|
419
419
|
className: "text-[10px] font-semibold uppercase tracking-wider text-text-tertiary mb-1.5",
|
|
420
420
|
children: e
|