@burdenoff/microfe-bigconsole 2026.729.1 → 2026.729.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/bigconsole/components/DrilldownDashboardRenderer.js +1 -1
  2. package/dist/bigconsole/components/DrilldownDashboardRenderer.js.map +1 -1
  3. package/dist/bigconsole/components/builder/inspector/DataBindingSection.js +4 -4
  4. package/dist/bigconsole/components/builder/inspector/DataBindingSection.js.map +1 -1
  5. package/dist/bigconsole/components/datasink/CacheTTLSelector.js +5 -5
  6. package/dist/bigconsole/components/datasink/CacheTTLSelector.js.map +1 -1
  7. package/dist/bigconsole/components/datasink/DataSinkCard.js +3 -3
  8. package/dist/bigconsole/components/datasink/DataSinkCard.js.map +1 -1
  9. package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js +4 -4
  10. package/dist/bigconsole/components/datasink/DataSinkDeleteDialog.js.map +1 -1
  11. package/dist/bigconsole/components/datasink/DataSinkStatusBadge.js +6 -6
  12. package/dist/bigconsole/components/datasink/DataSinkStatusBadge.js.map +1 -1
  13. package/dist/bigconsole/components/datasink/LinkedWorkflowsPanel.js +9 -9
  14. package/dist/bigconsole/components/datasink/LinkedWorkflowsPanel.js.map +1 -1
  15. package/dist/bigconsole/components/datasink/RefreshTypeSelector.js +2 -2
  16. package/dist/bigconsole/components/datasink/RefreshTypeSelector.js.map +1 -1
  17. package/dist/bigconsole/components/datasink/SetupStateBadge.js +8 -8
  18. package/dist/bigconsole/components/datasink/SetupStateBadge.js.map +1 -1
  19. package/dist/bigconsole/components/datasink/WorkflowStatusPanel.js +13 -13
  20. package/dist/bigconsole/components/datasink/WorkflowStatusPanel.js.map +1 -1
  21. package/dist/bigconsole/components/parser/DataSinkSelector.js +2 -2
  22. package/dist/bigconsole/components/parser/DataSinkSelector.js.map +1 -1
  23. package/dist/bigconsole/components/parser/ExpressionEditor.js +1 -1
  24. package/dist/bigconsole/components/parser/ExpressionEditor.js.map +1 -1
  25. package/dist/bigconsole/components/parser/ParserCard.js +1 -1
  26. package/dist/bigconsole/components/parser/ParserCard.js.map +1 -1
  27. package/dist/bigconsole/components/parser/ParserSelector.js +2 -2
  28. package/dist/bigconsole/components/parser/ParserSelector.js.map +1 -1
  29. package/dist/bigconsole/components/parser/ParserStatusBadge.js +2 -2
  30. package/dist/bigconsole/components/parser/ParserStatusBadge.js.map +1 -1
  31. package/dist/bigconsole/components/pipeline/PipelineCard.js +2 -2
  32. package/dist/bigconsole/components/pipeline/PipelineCard.js.map +1 -1
  33. package/dist/bigconsole/components/pipeline/PipelineDeleteDialog.js +5 -5
  34. package/dist/bigconsole/components/pipeline/PipelineDeleteDialog.js.map +1 -1
  35. package/dist/bigconsole/components/pipeline/PipelineStatusBadge.js +3 -3
  36. package/dist/bigconsole/components/pipeline/PipelineStatusBadge.js.map +1 -1
  37. package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js +1 -1
  38. package/dist/bigconsole/components/widgets/DrilldownConfigPanel.js.map +1 -1
  39. package/dist/bigconsole/components/widgets/PropertiesPanel.js +3 -3
  40. package/dist/bigconsole/components/widgets/PropertiesPanel.js.map +1 -1
  41. package/dist/bigconsole/components/widgets/WidgetErrorBoundary.js +2 -2
  42. package/dist/bigconsole/components/widgets/WidgetErrorBoundary.js.map +1 -1
  43. package/dist/bigconsole/components/widgets/WidgetMenu.js +1 -1
  44. package/dist/bigconsole/components/widgets/WidgetMenu.js.map +1 -1
  45. package/dist/bigconsole/components/widgets/adaptive-card/AdaptiveCardWidget.js +3 -3
  46. package/dist/bigconsole/components/widgets/adaptive-card/AdaptiveCardWidget.js.map +1 -1
  47. package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js +1 -1
  48. package/dist/bigconsole/components/widgets/calendar/CalendarWidget.js.map +1 -1
  49. package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js +3 -3
  50. package/dist/bigconsole/components/widgets/gauge/GaugeConfig.js.map +1 -1
  51. package/dist/bigconsole/components/widgets/heatmap/HeatmapWidget.js +52 -32
  52. package/dist/bigconsole/components/widgets/heatmap/HeatmapWidget.js.map +1 -1
  53. package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js +3 -3
  54. package/dist/bigconsole/components/widgets/kanban/KanbanWidget.js.map +1 -1
  55. package/dist/bigconsole/components/widgets/kpi/KPIWidget.js +1 -1
  56. package/dist/bigconsole/components/widgets/kpi/KPIWidget.js.map +1 -1
  57. package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js +2 -2
  58. package/dist/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.js.map +1 -1
  59. package/dist/bigconsole/components/widgets/list/ListItem.js +3 -3
  60. package/dist/bigconsole/components/widgets/list/ListItem.js.map +1 -1
  61. package/dist/bigconsole/components/widgets/metric-card/ComparisonBadge.js +1 -1
  62. package/dist/bigconsole/components/widgets/metric-card/ComparisonBadge.js.map +1 -1
  63. package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js +1 -1
  64. package/dist/bigconsole/components/widgets/metric-card/MetricCardWidget.js.map +1 -1
  65. package/dist/bigconsole/components/widgets/metric-card/SparklineChart.js +6 -6
  66. package/dist/bigconsole/components/widgets/metric-card/SparklineChart.js.map +1 -1
  67. package/dist/bigconsole/components/widgets/metric-card/TrendIndicator.js +1 -1
  68. package/dist/bigconsole/components/widgets/metric-card/TrendIndicator.js.map +1 -1
  69. package/dist/bigconsole/components/widgets/progress/ProgressBar.js +4 -4
  70. package/dist/bigconsole/components/widgets/progress/ProgressBar.js.map +1 -1
  71. package/dist/bigconsole/components/widgets/progress/ProgressConfig.js +1 -1
  72. package/dist/bigconsole/components/widgets/progress/ProgressConfig.js.map +1 -1
  73. package/dist/bigconsole/components/widgets/progress/ProgressWidget.js +1 -1
  74. package/dist/bigconsole/components/widgets/progress/ProgressWidget.js.map +1 -1
  75. package/dist/bigconsole/components/widgets/retention/RetentionWidget.js +2 -2
  76. package/dist/bigconsole/components/widgets/retention/RetentionWidget.js.map +1 -1
  77. package/dist/bigconsole/components/widgets/states/ErrorState.js +2 -2
  78. package/dist/bigconsole/components/widgets/states/ErrorState.js.map +1 -1
  79. package/dist/bigconsole/components/widgets/table/TableConfig.js +2 -2
  80. package/dist/bigconsole/components/widgets/table/TableConfig.js.map +1 -1
  81. package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js +1 -1
  82. package/dist/bigconsole/components/widgets/timeline/TimelineWidget.js.map +1 -1
  83. package/dist/bigconsole/components/widgets/widget-actions/WidgetActions.js +6 -6
  84. package/dist/bigconsole/components/widgets/widget-actions/WidgetActions.js.map +1 -1
  85. package/dist/bigconsole/components/workflow/WorkflowCard.js +1 -1
  86. package/dist/bigconsole/components/workflow/WorkflowCard.js.map +1 -1
  87. package/dist/bigconsole/components/workflow/WorkflowDeleteDialog.js +5 -5
  88. package/dist/bigconsole/components/workflow/WorkflowDeleteDialog.js.map +1 -1
  89. package/dist/bigconsole/components/workflow/WorkflowStatusBadge.js +6 -6
  90. package/dist/bigconsole/components/workflow/WorkflowStatusBadge.js.map +1 -1
  91. package/dist/bigconsole/pages/DataSinkBuilderPage.js +17 -17
  92. package/dist/bigconsole/pages/DataSinkBuilderPage.js.map +1 -1
  93. package/dist/bigconsole/pages/DataSinkViewPage.js +2 -2
  94. package/dist/bigconsole/pages/DataSinkViewPage.js.map +1 -1
  95. package/dist/bigconsole/pages/ParserBuilderPage.js +24 -24
  96. package/dist/bigconsole/pages/ParserBuilderPage.js.map +1 -1
  97. package/dist/bigconsole/pages/ParserViewPage.js +8 -8
  98. package/dist/bigconsole/pages/ParserViewPage.js.map +1 -1
  99. package/dist/bigconsole/pages/PipelineBuilderPage.js +9 -9
  100. package/dist/bigconsole/pages/PipelineBuilderPage.js.map +1 -1
  101. package/dist/bigconsole/pages/PipelineViewPage.js +12 -12
  102. package/dist/bigconsole/pages/PipelineViewPage.js.map +1 -1
  103. package/dist/bigconsole/pages/WorkflowViewPage.js +6 -6
  104. package/dist/bigconsole/pages/WorkflowViewPage.js.map +1 -1
  105. package/dist/bigconsole/renderers/adapters/AdaptiveCardRenderer.js +1 -1
  106. package/dist/bigconsole/renderers/adapters/AdaptiveCardRenderer.js.map +1 -1
  107. package/dist/bigconsole/renderers/adapters/BlockKitRenderer.js +2 -2
  108. package/dist/bigconsole/renderers/adapters/BlockKitRenderer.js.map +1 -1
  109. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-state-error/20 text-state-error';\n case 'medium':\n return 'bg-state-warning/20 text-state-warning';\n case 'low':\n return 'bg-state-success/20 text-state-success';\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// Field names we try, in order, when the widget config doesn't name one\n// explicitly. A DataSink/parser typically emits a flat array of rows (e.g. the\n// Linear \"Kanban board\" parser -> [{ id, title, status, assignee, ... }]), so\n// the board is derived from a status/column field on those rows.\nconst COLUMN_FIELD_CANDIDATES = ['status', 'state', 'column', 'stage', 'category', 'phase', 'group'];\nconst TITLE_FIELD_CANDIDATES = ['title', 'name', 'summary', 'label', 'identifier', 'id'];\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/** Coerce parser output into an array of object rows (handles common envelopes). */\nfunction asRows(data: unknown): Record<string, unknown>[] {\n const onlyObjects = (arr: unknown[]): Record<string, unknown>[] =>\n arr.filter((r): r is Record<string, unknown> => !!r && typeof r === 'object' && !Array.isArray(r));\n\n if (Array.isArray(data)) return onlyObjects(data);\n\n if (data && typeof data === 'object') {\n const obj = data as Record<string, unknown>;\n for (const key of ['tasks', 'rows', 'items', 'data', 'nodes', 'results', 'records']) {\n if (Array.isArray(obj[key])) return onlyObjects(obj[key] as unknown[]);\n }\n const firstArray = Object.values(obj).find((v) => Array.isArray(v));\n if (Array.isArray(firstArray)) return onlyObjects(firstArray);\n }\n return [];\n}\n\n/** First key that actually exists on the sample row. */\nfunction firstPresent(row: Record<string, unknown>, keys: (string | undefined)[]): string | undefined {\n for (const key of keys) {\n if (key && key in row) return key;\n }\n return undefined;\n}\n\nfunction normalizePriority(value: unknown): KanbanTask['priority'] {\n const v = asString(value)?.toLowerCase();\n if (!v) return undefined;\n if (v === 'urgent' || v === 'high') return 'high';\n if (v === 'medium' || v === 'normal') return 'medium';\n if (v === 'low') return 'low';\n return undefined;\n}\n\n/**\n * Normalize widget data into the board's { columns, tasks } shape.\n *\n * KanbanWidget previously required the exact canonical shape and rendered\n * \"No board data\" for any DataSink/parser output that was a plain array of\n * rows (which is what parsers actually emit) — BOFF-4110. This adapts a flat\n * array into columns (distinct values of a status/column field, in first-seen\n * order) plus one card per row, honoring config field mappings with fallbacks.\n */\nfunction normalizeKanban(\n data: unknown,\n config?: Record<string, unknown>\n): { columns: KanbanColumn[]; tasks: KanbanTask[] } {\n // Canonical shape passes through untouched (back-compat).\n if (data && typeof data === 'object' && !Array.isArray(data) && Array.isArray((data as KanbanData).columns)) {\n const d = data as KanbanData;\n return { columns: d.columns ?? [], tasks: d.tasks ?? [] };\n }\n\n const rows = asRows(data);\n if (rows.length === 0) return { columns: [], tasks: [] };\n\n const sample = rows[0];\n const columnField = firstPresent(sample, [\n asString(config?.columnField),\n asString(config?.groupField),\n asString(config?.statusField),\n ...COLUMN_FIELD_CANDIDATES,\n asString(config?.xAxisField),\n asString(config?.labelField),\n ]);\n if (!columnField) return { columns: [], tasks: [] };\n\n const titleField = firstPresent(sample, [\n asString(config?.cardTitleField),\n asString(config?.titleField),\n ...TITLE_FIELD_CANDIDATES,\n asString(config?.labelField),\n ]);\n\n const columnTitles: Record<string, string> = {};\n const columnOrder: string[] = [];\n const tasks: KanbanTask[] = rows.map((row, i) => {\n const columnRaw = row[columnField];\n const column = columnRaw == null || columnRaw === '' ? 'Uncategorized' : String(columnRaw);\n if (!(column in columnTitles)) {\n columnTitles[column] = column;\n columnOrder.push(column);\n }\n const idRaw = row.id ?? row.identifier ?? row.key;\n const labels = Array.isArray(row.labels) ? row.labels.filter((l): l is string => typeof l === 'string') : undefined;\n return {\n id: idRaw != null ? String(idRaw) : `task-${i}`,\n title: titleField && row[titleField] != null ? String(row[titleField]) : `Item ${i + 1}`,\n column,\n assignee: asString(row.assignee),\n priority: normalizePriority(row.priority ?? row.priorityLabel),\n labels: labels && labels.length > 0 ? labels : undefined,\n };\n });\n\n const columns: KanbanColumn[] = columnOrder.map((id) => ({ id, title: columnTitles[id] ?? id }));\n return { columns, tasks };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget, data, onClick }) {\n const config = widget.config as Record<string, unknown> | undefined;\n const { columns, tasks } = useMemo(() => normalizeKanban(data, config), [data, config]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>No board data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-tertiary\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && <p className=\"text-[10px] text-text-tertiary text-center py-4\">No tasks</p>}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"KanbanWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kanban/KanbanWidget.tsx"],"sourcesContent":["/**\n * KanbanWidget Component\n *\n * Simple kanban board view with columns and tasks.\n */\n\nimport { type FC, memo, useMemo } from 'react';\nimport type { Widget } from '../../../types';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KanbanWidgetProps {\n widget: Widget;\n data?: KanbanData;\n onClick?: (task: KanbanTask) => void;\n}\n\nexport interface KanbanColumn {\n id: string;\n title: string;\n color?: string;\n}\n\nexport interface KanbanTask {\n id: string;\n title: string;\n column: string;\n priority?: 'high' | 'medium' | 'low';\n assignee?: string;\n labels?: string[];\n dueDate?: string;\n}\n\nexport interface KanbanData {\n columns?: KanbanColumn[];\n tasks?: KanbanTask[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getPriorityColor(priority?: string): string {\n switch (priority) {\n case 'high':\n return 'bg-status-error-bg/20 text-status-error-text';\n case 'medium':\n return 'bg-status-warning-bg/20 text-status-warning-text';\n case 'low':\n return 'bg-status-success-bg/20 text-status-success-text';\n default:\n return 'bg-bg-sunken text-text-secondary';\n }\n}\n\n// Field names we try, in order, when the widget config doesn't name one\n// explicitly. A DataSink/parser typically emits a flat array of rows (e.g. the\n// Linear \"Kanban board\" parser -> [{ id, title, status, assignee, ... }]), so\n// the board is derived from a status/column field on those rows.\nconst COLUMN_FIELD_CANDIDATES = ['status', 'state', 'column', 'stage', 'category', 'phase', 'group'];\nconst TITLE_FIELD_CANDIDATES = ['title', 'name', 'summary', 'label', 'identifier', 'id'];\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\n/** Coerce parser output into an array of object rows (handles common envelopes). */\nfunction asRows(data: unknown): Record<string, unknown>[] {\n const onlyObjects = (arr: unknown[]): Record<string, unknown>[] =>\n arr.filter((r): r is Record<string, unknown> => !!r && typeof r === 'object' && !Array.isArray(r));\n\n if (Array.isArray(data)) return onlyObjects(data);\n\n if (data && typeof data === 'object') {\n const obj = data as Record<string, unknown>;\n for (const key of ['tasks', 'rows', 'items', 'data', 'nodes', 'results', 'records']) {\n if (Array.isArray(obj[key])) return onlyObjects(obj[key] as unknown[]);\n }\n const firstArray = Object.values(obj).find((v) => Array.isArray(v));\n if (Array.isArray(firstArray)) return onlyObjects(firstArray);\n }\n return [];\n}\n\n/** First key that actually exists on the sample row. */\nfunction firstPresent(row: Record<string, unknown>, keys: (string | undefined)[]): string | undefined {\n for (const key of keys) {\n if (key && key in row) return key;\n }\n return undefined;\n}\n\nfunction normalizePriority(value: unknown): KanbanTask['priority'] {\n const v = asString(value)?.toLowerCase();\n if (!v) return undefined;\n if (v === 'urgent' || v === 'high') return 'high';\n if (v === 'medium' || v === 'normal') return 'medium';\n if (v === 'low') return 'low';\n return undefined;\n}\n\n/**\n * Normalize widget data into the board's { columns, tasks } shape.\n *\n * KanbanWidget previously required the exact canonical shape and rendered\n * \"No board data\" for any DataSink/parser output that was a plain array of\n * rows (which is what parsers actually emit) — BOFF-4110. This adapts a flat\n * array into columns (distinct values of a status/column field, in first-seen\n * order) plus one card per row, honoring config field mappings with fallbacks.\n */\nfunction normalizeKanban(\n data: unknown,\n config?: Record<string, unknown>\n): { columns: KanbanColumn[]; tasks: KanbanTask[] } {\n // Canonical shape passes through untouched (back-compat).\n if (data && typeof data === 'object' && !Array.isArray(data) && Array.isArray((data as KanbanData).columns)) {\n const d = data as KanbanData;\n return { columns: d.columns ?? [], tasks: d.tasks ?? [] };\n }\n\n const rows = asRows(data);\n if (rows.length === 0) return { columns: [], tasks: [] };\n\n const sample = rows[0];\n const columnField = firstPresent(sample, [\n asString(config?.columnField),\n asString(config?.groupField),\n asString(config?.statusField),\n ...COLUMN_FIELD_CANDIDATES,\n asString(config?.xAxisField),\n asString(config?.labelField),\n ]);\n if (!columnField) return { columns: [], tasks: [] };\n\n const titleField = firstPresent(sample, [\n asString(config?.cardTitleField),\n asString(config?.titleField),\n ...TITLE_FIELD_CANDIDATES,\n asString(config?.labelField),\n ]);\n\n const columnTitles: Record<string, string> = {};\n const columnOrder: string[] = [];\n const tasks: KanbanTask[] = rows.map((row, i) => {\n const columnRaw = row[columnField];\n const column = columnRaw == null || columnRaw === '' ? 'Uncategorized' : String(columnRaw);\n if (!(column in columnTitles)) {\n columnTitles[column] = column;\n columnOrder.push(column);\n }\n const idRaw = row.id ?? row.identifier ?? row.key;\n const labels = Array.isArray(row.labels) ? row.labels.filter((l): l is string => typeof l === 'string') : undefined;\n return {\n id: idRaw != null ? String(idRaw) : `task-${i}`,\n title: titleField && row[titleField] != null ? String(row[titleField]) : `Item ${i + 1}`,\n column,\n assignee: asString(row.assignee),\n priority: normalizePriority(row.priority ?? row.priorityLabel),\n labels: labels && labels.length > 0 ? labels : undefined,\n };\n });\n\n const columns: KanbanColumn[] = columnOrder.map((id) => ({ id, title: columnTitles[id] ?? id }));\n return { columns, tasks };\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KanbanWidget: FC<KanbanWidgetProps> = memo(function KanbanWidget({ widget, data, onClick }) {\n const config = widget.config as Record<string, unknown> | undefined;\n const { columns, tasks } = useMemo(() => normalizeKanban(data, config), [data, config]);\n\n // Group tasks by column\n const tasksByColumn = useMemo(() => {\n const grouped: Record<string, KanbanTask[]> = {};\n columns.forEach((col) => {\n grouped[col.id] = tasks.filter((t) => t.column === col.id);\n });\n return grouped;\n }, [columns, tasks]);\n\n if (!columns.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2\"\n />\n </svg>\n <p>No board data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full p-2 gap-2 overflow-x-auto\">\n {columns.map((column) => {\n const columnTasks = tasksByColumn[column.id] || [];\n\n return (\n <div key={column.id} className=\"flex-shrink-0 w-48 flex flex-col bg-bg-sunken/50 rounded-lg\">\n {/* Column Header */}\n <div className=\"flex items-center gap-2 p-2 border-b border-border-default\">\n <div\n className=\"w-2 h-2 rounded-full\"\n style={{ backgroundColor: column.color || 'var(--color-text-muted)' }}\n />\n <span className=\"text-xs font-medium text-text-primary truncate\">{column.title}</span>\n <span className=\"ml-auto text-xs text-text-tertiary\">{columnTasks.length}</span>\n </div>\n\n {/* Tasks */}\n <div className=\"flex-1 p-2 space-y-2 overflow-y-auto\">\n {columnTasks.map((task) => (\n <div\n key={task.id}\n className={`\n p-2 bg-bg-surface rounded border border-border-default\n ${onClick ? 'cursor-pointer hover:border-action-primary-bg' : ''}\n transition-colors\n `}\n onClick={() => onClick?.(task)}\n >\n <p className=\"text-xs font-medium text-text-primary line-clamp-2\">{task.title}</p>\n\n <div className=\"flex items-center gap-1 mt-2 flex-wrap\">\n {task.priority && (\n <span className={`px-1.5 py-0.5 text-[10px] rounded ${getPriorityColor(task.priority)}`}>\n {task.priority}\n </span>\n )}\n {task.assignee && (\n <span className=\"px-1.5 py-0.5 text-[10px] rounded bg-action-primary-bg/10 text-action-primary-text\">\n {task.assignee}\n </span>\n )}\n </div>\n </div>\n ))}\n\n {columnTasks.length === 0 && <p className=\"text-[10px] text-text-tertiary text-center py-4\">No tasks</p>}\n </div>\n </div>\n );\n })}\n </div>\n );\n});\n\nexport default KanbanWidget;\n"],"mappings":";;;AA4CA,SAAS,EAAiB,GAA2B;AACnD,SAAQ,GAAR;EACE,KAAK,OACH,QAAO;EACT,KAAK,SACH,QAAO;EACT,KAAK,MACH,QAAO;EACT,QACE,QAAO;;;AAQb,IAAM,IAA0B;CAAC;CAAU;CAAS;CAAU;CAAS;CAAY;CAAS;CAAQ,EAC9F,IAAyB;CAAC;CAAS;CAAQ;CAAW;CAAS;CAAc;CAAK;AAExF,SAAS,EAAS,GAAoC;AACpD,QAAO,OAAO,KAAU,YAAY,EAAM,SAAS,IAAI,IAAQ,KAAA;;AAIjE,SAAS,EAAO,GAA0C;CACxD,IAAM,KAAe,MACnB,EAAI,QAAQ,MAAoC,CAAC,CAAC,KAAK,OAAO,KAAM,YAAY,CAAC,MAAM,QAAQ,EAAE,CAAC;AAEpG,KAAI,MAAM,QAAQ,EAAK,CAAE,QAAO,EAAY,EAAK;AAEjD,KAAI,KAAQ,OAAO,KAAS,UAAU;EACpC,IAAM,IAAM;AACZ,OAAK,IAAM,KAAO;GAAC;GAAS;GAAQ;GAAS;GAAQ;GAAS;GAAW;GAAU,CACjF,KAAI,MAAM,QAAQ,EAAI,GAAK,CAAE,QAAO,EAAY,EAAI,GAAkB;EAExE,IAAM,IAAa,OAAO,OAAO,EAAI,CAAC,MAAM,MAAM,MAAM,QAAQ,EAAE,CAAC;AACnE,MAAI,MAAM,QAAQ,EAAW,CAAE,QAAO,EAAY,EAAW;;AAE/D,QAAO,EAAE;;AAIX,SAAS,EAAa,GAA8B,GAAkD;AACpG,MAAK,IAAM,KAAO,EAChB,KAAI,KAAO,KAAO,EAAK,QAAO;;AAKlC,SAAS,EAAkB,GAAwC;CACjE,IAAM,IAAI,EAAS,EAAM,EAAE,aAAa;AACnC,QACL;MAAI,MAAM,YAAY,MAAM,OAAQ,QAAO;AAC3C,MAAI,MAAM,YAAY,MAAM,SAAU,QAAO;AAC7C,MAAI,MAAM,MAAO,QAAO;;;AAa1B,SAAS,EACP,GACA,GACkD;AAElD,KAAI,KAAQ,OAAO,KAAS,YAAY,CAAC,MAAM,QAAQ,EAAK,IAAI,MAAM,QAAS,EAAoB,QAAQ,EAAE;EAC3G,IAAM,IAAI;AACV,SAAO;GAAE,SAAS,EAAE,WAAW,EAAE;GAAE,OAAO,EAAE,SAAS,EAAE;GAAE;;CAG3D,IAAM,IAAO,EAAO,EAAK;AACzB,KAAI,EAAK,WAAW,EAAG,QAAO;EAAE,SAAS,EAAE;EAAE,OAAO,EAAE;EAAE;CAExD,IAAM,IAAS,EAAK,IACd,IAAc,EAAa,GAAQ;EACvC,EAAS,GAAQ,YAAY;EAC7B,EAAS,GAAQ,WAAW;EAC5B,EAAS,GAAQ,YAAY;EAC7B,GAAG;EACH,EAAS,GAAQ,WAAW;EAC5B,EAAS,GAAQ,WAAW;EAC7B,CAAC;AACF,KAAI,CAAC,EAAa,QAAO;EAAE,SAAS,EAAE;EAAE,OAAO,EAAE;EAAE;CAEnD,IAAM,IAAa,EAAa,GAAQ;EACtC,EAAS,GAAQ,eAAe;EAChC,EAAS,GAAQ,WAAW;EAC5B,GAAG;EACH,EAAS,GAAQ,WAAW;EAC7B,CAAC,EAEI,IAAuC,EAAE,EACzC,IAAwB,EAAE,EAC1B,IAAsB,EAAK,KAAK,GAAK,MAAM;EAC/C,IAAM,IAAY,EAAI,IAChB,IAAS,KAAa,QAAQ,MAAc,KAAK,kBAAkB,OAAO,EAAU;AAC1F,EAAM,KAAU,MACd,EAAa,KAAU,GACvB,EAAY,KAAK,EAAO;EAE1B,IAAM,IAAQ,EAAI,MAAM,EAAI,cAAc,EAAI,KACxC,IAAS,MAAM,QAAQ,EAAI,OAAO,GAAG,EAAI,OAAO,QAAQ,MAAmB,OAAO,KAAM,SAAS,GAAG,KAAA;AAC1G,SAAO;GACL,IAAI,KAAS,OAAuB,QAAQ,MAAxB,OAAO,EAAM;GACjC,OAAO,KAAc,EAAI,MAAe,OAAO,OAAO,EAAI,GAAY,GAAG,QAAQ,IAAI;GACrF;GACA,UAAU,EAAS,EAAI,SAAS;GAChC,UAAU,EAAkB,EAAI,YAAY,EAAI,cAAc;GAC9D,QAAQ,KAAU,EAAO,SAAS,IAAI,IAAS,KAAA;GAChD;GACD;AAGF,QAAO;EAAE,SADuB,EAAY,KAAK,OAAQ;GAAE;GAAI,OAAO,EAAa,MAAO;GAAI,EAAE;EAC9E;EAAO;;AAO3B,IAAa,IAAsC,EAAK,SAAsB,EAAE,WAAQ,SAAM,cAAW;CACvG,IAAM,IAAS,EAAO,QAChB,EAAE,YAAS,aAAU,QAAc,EAAgB,GAAM,EAAO,EAAE,CAAC,GAAM,EAAO,CAAC,EAGjF,IAAgB,QAAc;EAClC,IAAM,IAAwC,EAAE;AAIhD,SAHA,EAAQ,SAAS,MAAQ;AACvB,KAAQ,EAAI,MAAM,EAAM,QAAQ,MAAM,EAAE,WAAW,EAAI,GAAG;IAC1D,EACK;IACN,CAAC,GAAS,EAAM,CAAC;AAyBpB,QAvBK,EAAQ,SAwBX,kBAAC,OAAD;EAAK,WAAU;YACZ,EAAQ,KAAK,MAAW;GACvB,IAAM,IAAc,EAAc,EAAO,OAAO,EAAE;AAElD,UACE,kBAAC,OAAD;IAAqB,WAAU;cAA/B,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,iBAAiB,EAAO,SAAS,2BAA2B;OACrE,CAAA;MACF,kBAAC,QAAD;OAAM,WAAU;iBAAkD,EAAO;OAAa,CAAA;MACtF,kBAAC,QAAD;OAAM,WAAU;iBAAsC,EAAY;OAAc,CAAA;MAC5E;QAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,EAAY,KAAK,MAChB,kBAAC,OAAD;MAEE,WAAW;;wBAEL,IAAU,kDAAkD,GAAG;;;MAGrE,eAAe,IAAU,EAAK;gBAPhC,CASE,kBAAC,KAAD;OAAG,WAAU;iBAAsD,EAAK;OAAU,CAAA,EAElF,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAW,qCAAqC,EAAiB,EAAK,SAAS;kBAClF,EAAK;QACD,CAAA,EAER,EAAK,YACJ,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAK;QACD,CAAA,CAEL;SACF;QAtBC,EAAK,GAsBN,CACN,EAED,EAAY,WAAW,KAAK,kBAAC,KAAD;MAAG,WAAU;gBAAkD;MAAY,CAAA,CACpG;OACF;MA1CI,EAAO,GA0CX;IAER;EACE,CAAA,GAxEJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,QAAO;IACP,SAAQ;cAER,kBAAC,QAAD;KACE,eAAc;KACd,gBAAe;KACf,aAAa;KACb,GAAE;KACF,CAAA;IACE,CAAA,EACN,kBAAC,KAAD,EAAA,UAAG,iBAAiB,CAAA,CAChB;;EACF,CAAA;EAyDV"}
@@ -65,7 +65,7 @@ t(function({ widget: t, data: s, onClick: c }) {
65
65
  size: "sm",
66
66
  positiveIsGood: l?.positiveIsGood !== !1
67
67
  }), /* @__PURE__ */ i("span", {
68
- className: `text-xs ${t.changePercent >= 0 ? l?.positiveIsGood === !1 ? "text-state-error" : "text-state-success" : l?.positiveIsGood === !1 ? "text-state-success" : "text-state-error"}`,
68
+ className: `text-xs ${t.changePercent >= 0 ? l?.positiveIsGood === !1 ? "text-status-error-text" : "text-status-success-text" : l?.positiveIsGood === !1 ? "text-status-success-text" : "text-status-error-text"}`,
69
69
  children: [
70
70
  t.changePercent > 0 ? "+" : "",
71
71
  t.changePercent.toFixed(1),
@@ -1 +1 @@
1
- {"version":3,"file":"KPIWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi/KPIWidget.tsx"],"sourcesContent":["/**\n * KPIWidget Component\n *\n * Multi-KPI comparison card showing multiple metrics together.\n */\n\nimport { type FC, memo, useCallback } from 'react';\nimport type { Widget, KPIConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from '../metric-card/TrendIndicator';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIWidgetProps {\n widget: Widget;\n data?: KPIData;\n onClick?: (metricIndex: number) => void;\n}\n\nexport interface KPIMetric {\n label: string;\n value: number | string;\n previousValue?: number;\n changePercent?: number;\n trend?: TrendDirection;\n unit?: string;\n}\n\nexport interface KPIData {\n metrics: KPIMetric[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number | string, format?: string): string {\n if (typeof value === 'string') return value;\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'percentage':\n return `${(value * 100).toFixed(1)}%`;\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n default:\n return new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n}\n\nfunction getTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIWidget: FC<KPIWidgetProps> = memo(function KPIWidget({ widget, data, onClick }) {\n const config = widget.config as unknown as KPIConfig | undefined;\n\n const handleMetricClick = useCallback(\n (index: number) => {\n if (onClick && widget.drilldowns?.length) {\n onClick(index);\n }\n },\n [onClick, widget.drilldowns]\n );\n\n const isClickable = Boolean(onClick && widget.drilldowns?.length);\n\n if (!data?.metrics?.length) {\n return null;\n }\n\n const layout = config?.layout || 'horizontal';\n\n return (\n <div className=\"flex flex-col h-full p-4\">\n <div\n className={`\n grid gap-4\n ${layout === 'horizontal' ? 'md:grid-cols-2 lg:grid-cols-4' : 'grid-cols-1'}\n flex-1\n `}\n >\n {data.metrics.map((metric, index) => (\n <div\n key={index}\n className={`\n flex flex-col p-4\n bg-bg-sunken/50\n rounded-lg\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken transition-colors' : ''}\n `}\n onClick={() => handleMetricClick(index)}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary uppercase tracking-wide mb-1\">{metric.label}</p>\n\n {/* Value */}\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold text-text-primary\">{formatValue(metric.value, config?.format)}</span>\n {metric.unit && <span className=\"text-sm text-text-secondary\">{metric.unit}</span>}\n </div>\n\n {/* Change indicator */}\n {metric.changePercent !== undefined && (\n <div className=\"flex items-center gap-1 mt-2\">\n <TrendIndicator\n direction={metric.trend || getTrend(metric.changePercent)}\n size=\"sm\"\n positiveIsGood={config?.positiveIsGood !== false}\n />\n <span\n className={`text-xs ${\n metric.changePercent >= 0\n ? config?.positiveIsGood !== false\n ? 'text-state-success'\n : 'text-state-error'\n : config?.positiveIsGood !== false\n ? 'text-state-error'\n : 'text-state-success'\n }`}\n >\n {metric.changePercent > 0 ? '+' : ''}\n {metric.changePercent.toFixed(1)}%\n </span>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIWidget;\n"],"mappings":";;;;AAqCA,SAAS,EAAY,GAAwB,GAAyB;AACpE,KAAI,OAAO,KAAU,SAAU,QAAO;AAEtC,SAAQ,GAAR;EACE,KAAK,WACH,QAAO,IAAI,KAAK,aAAa,SAAS;GACpC,OAAO;GACP,UAAU;GACV,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;EAClB,KAAK,aACH,QAAO,IAAI,IAAQ,KAAK,QAAQ,EAAE,CAAC;EACrC,KAAK,UACH,QAAO,IAAI,KAAK,aAAa,SAAS;GACpC,UAAU;GACV,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;EAClB,QACE,QAAO,IAAI,KAAK,aAAa,SAAS,EACpC,uBAAuB,GACxB,CAAC,CAAC,OAAO,EAAM;;;AAItB,SAAS,EAAS,GAAwC;AAExD,QADI,MAAkB,KAAA,KAAa,MAAkB,IAAU,SACxD,IAAgB,IAAI,OAAO;;AAOS,EAAK,SAAmB,EAAE,WAAQ,SAAM,cAAW;CAC9F,IAAM,IAAS,EAAO,QAEhB,IAAoB,GACvB,MAAkB;AACjB,EAAI,KAAW,EAAO,YAAY,UAChC,EAAQ,EAAM;IAGlB,CAAC,GAAS,EAAO,WAAW,CAC7B,EAEK,IAAc,GAAQ,KAAW,EAAO,YAAY;AAQ1D,QANK,GAAM,SAAS,SAOlB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GACE,WAAW;;aALF,GAAQ,UAAU,kBAOZ,eAAe,kCAAkC,cAAc;;;aAI7E,EAAK,QAAQ,KAAK,GAAQ,MACzB,kBAAC,OAAD;IAEE,WAAW;;;;gBAIP,IAAc,wDAAwD,GAAG;;IAE7E,eAAe,EAAkB,EAAM;IACvC,MAAM,IAAc,WAAW,KAAA;IAC/B,UAAU,IAAc,IAAI,KAAA;cAV9B;KAaE,kBAAC,KAAD;MAAG,WAAU;gBAA4D,EAAO;MAAU,CAAA;KAG1F,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAwC,EAAY,EAAO,OAAO,GAAQ,OAAO;OAAQ,CAAA,EACxG,EAAO,QAAQ,kBAAC,QAAD;OAAM,WAAU;iBAA+B,EAAO;OAAY,CAAA,CAC9E;;KAGL,EAAO,kBAAkB,KAAA,KACxB,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,WAAW,EAAO,SAAS,EAAS,EAAO,cAAc;OACzD,MAAK;OACL,gBAAgB,GAAQ,mBAAmB;OAC3C,CAAA,EACF,kBAAC,QAAD;OACE,WAAW,WACT,EAAO,iBAAiB,IACpB,GAAQ,mBAAmB,KAEzB,qBADA,uBAEF,GAAQ,mBAAmB,KAEzB,uBADA;iBAPV;QAWG,EAAO,gBAAgB,IAAI,MAAM;QACjC,EAAO,cAAc,QAAQ,EAAE;QAAC;QAC5B;SACH;;KAEJ;MA5CC,EA4CD,CACN;GACE,CAAA;EACF,CAAA,GA/DC;EAiET"}
1
+ {"version":3,"file":"KPIWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi/KPIWidget.tsx"],"sourcesContent":["/**\n * KPIWidget Component\n *\n * Multi-KPI comparison card showing multiple metrics together.\n */\n\nimport { type FC, memo, useCallback } from 'react';\nimport type { Widget, KPIConfig } from '../../../types';\nimport { TrendIndicator, type TrendDirection } from '../metric-card/TrendIndicator';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIWidgetProps {\n widget: Widget;\n data?: KPIData;\n onClick?: (metricIndex: number) => void;\n}\n\nexport interface KPIMetric {\n label: string;\n value: number | string;\n previousValue?: number;\n changePercent?: number;\n trend?: TrendDirection;\n unit?: string;\n}\n\nexport interface KPIData {\n metrics: KPIMetric[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number | string, format?: string): string {\n if (typeof value === 'string') return value;\n\n switch (format) {\n case 'currency':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n maximumFractionDigits: 0,\n }).format(value);\n case 'percentage':\n return `${(value * 100).toFixed(1)}%`;\n case 'compact':\n return new Intl.NumberFormat('en-US', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(value);\n default:\n return new Intl.NumberFormat('en-US', {\n maximumFractionDigits: 2,\n }).format(value);\n }\n}\n\nfunction getTrend(changePercent?: number): TrendDirection {\n if (changePercent === undefined || changePercent === 0) return 'flat';\n return changePercent > 0 ? 'up' : 'down';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIWidget: FC<KPIWidgetProps> = memo(function KPIWidget({ widget, data, onClick }) {\n const config = widget.config as unknown as KPIConfig | undefined;\n\n const handleMetricClick = useCallback(\n (index: number) => {\n if (onClick && widget.drilldowns?.length) {\n onClick(index);\n }\n },\n [onClick, widget.drilldowns]\n );\n\n const isClickable = Boolean(onClick && widget.drilldowns?.length);\n\n if (!data?.metrics?.length) {\n return null;\n }\n\n const layout = config?.layout || 'horizontal';\n\n return (\n <div className=\"flex flex-col h-full p-4\">\n <div\n className={`\n grid gap-4\n ${layout === 'horizontal' ? 'md:grid-cols-2 lg:grid-cols-4' : 'grid-cols-1'}\n flex-1\n `}\n >\n {data.metrics.map((metric, index) => (\n <div\n key={index}\n className={`\n flex flex-col p-4\n bg-bg-sunken/50\n rounded-lg\n ${isClickable ? 'cursor-pointer hover:bg-bg-sunken transition-colors' : ''}\n `}\n onClick={() => handleMetricClick(index)}\n role={isClickable ? 'button' : undefined}\n tabIndex={isClickable ? 0 : undefined}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary uppercase tracking-wide mb-1\">{metric.label}</p>\n\n {/* Value */}\n <div className=\"flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold text-text-primary\">{formatValue(metric.value, config?.format)}</span>\n {metric.unit && <span className=\"text-sm text-text-secondary\">{metric.unit}</span>}\n </div>\n\n {/* Change indicator */}\n {metric.changePercent !== undefined && (\n <div className=\"flex items-center gap-1 mt-2\">\n <TrendIndicator\n direction={metric.trend || getTrend(metric.changePercent)}\n size=\"sm\"\n positiveIsGood={config?.positiveIsGood !== false}\n />\n <span\n className={`text-xs ${\n metric.changePercent >= 0\n ? config?.positiveIsGood !== false\n ? 'text-status-success-text'\n : 'text-status-error-text'\n : config?.positiveIsGood !== false\n ? 'text-status-error-text'\n : 'text-status-success-text'\n }`}\n >\n {metric.changePercent > 0 ? '+' : ''}\n {metric.changePercent.toFixed(1)}%\n </span>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIWidget;\n"],"mappings":";;;;AAqCA,SAAS,EAAY,GAAwB,GAAyB;AACpE,KAAI,OAAO,KAAU,SAAU,QAAO;AAEtC,SAAQ,GAAR;EACE,KAAK,WACH,QAAO,IAAI,KAAK,aAAa,SAAS;GACpC,OAAO;GACP,UAAU;GACV,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;EAClB,KAAK,aACH,QAAO,IAAI,IAAQ,KAAK,QAAQ,EAAE,CAAC;EACrC,KAAK,UACH,QAAO,IAAI,KAAK,aAAa,SAAS;GACpC,UAAU;GACV,uBAAuB;GACxB,CAAC,CAAC,OAAO,EAAM;EAClB,QACE,QAAO,IAAI,KAAK,aAAa,SAAS,EACpC,uBAAuB,GACxB,CAAC,CAAC,OAAO,EAAM;;;AAItB,SAAS,EAAS,GAAwC;AAExD,QADI,MAAkB,KAAA,KAAa,MAAkB,IAAU,SACxD,IAAgB,IAAI,OAAO;;AAOS,EAAK,SAAmB,EAAE,WAAQ,SAAM,cAAW;CAC9F,IAAM,IAAS,EAAO,QAEhB,IAAoB,GACvB,MAAkB;AACjB,EAAI,KAAW,EAAO,YAAY,UAChC,EAAQ,EAAM;IAGlB,CAAC,GAAS,EAAO,WAAW,CAC7B,EAEK,IAAc,GAAQ,KAAW,EAAO,YAAY;AAQ1D,QANK,GAAM,SAAS,SAOlB,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GACE,WAAW;;aALF,GAAQ,UAAU,kBAOZ,eAAe,kCAAkC,cAAc;;;aAI7E,EAAK,QAAQ,KAAK,GAAQ,MACzB,kBAAC,OAAD;IAEE,WAAW;;;;gBAIP,IAAc,wDAAwD,GAAG;;IAE7E,eAAe,EAAkB,EAAM;IACvC,MAAM,IAAc,WAAW,KAAA;IAC/B,UAAU,IAAc,IAAI,KAAA;cAV9B;KAaE,kBAAC,KAAD;MAAG,WAAU;gBAA4D,EAAO;MAAU,CAAA;KAG1F,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAwC,EAAY,EAAO,OAAO,GAAQ,OAAO;OAAQ,CAAA,EACxG,EAAO,QAAQ,kBAAC,QAAD;OAAM,WAAU;iBAA+B,EAAO;OAAY,CAAA,CAC9E;;KAGL,EAAO,kBAAkB,KAAA,KACxB,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,WAAW,EAAO,SAAS,EAAS,EAAO,cAAc;OACzD,MAAK;OACL,gBAAgB,GAAQ,mBAAmB;OAC3C,CAAA,EACF,kBAAC,QAAD;OACE,WAAW,WACT,EAAO,iBAAiB,IACpB,GAAQ,mBAAmB,KAEzB,2BADA,6BAEF,GAAQ,mBAAmB,KAEzB,6BADA;iBAPV;QAWG,EAAO,gBAAgB,IAAI,MAAM;QACjC,EAAO,cAAc,QAAQ,EAAE;QAAC;QAC5B;SACH;;KAEJ;MA5CC,EA4CD,CACN;GACE,CAAA;EACF,CAAA,GA/DC;EAiET"}
@@ -14,7 +14,7 @@ function a(e, t) {
14
14
  }).format(e);
15
15
  }
16
16
  function o(e, t) {
17
- return !e || e === "stable" ? "text-text-secondary" : e === "up" && (t ?? 0) >= 0 ? "text-state-success" : e === "down" && (t ?? 0) < 0 ? "text-state-error" : "text-text-secondary";
17
+ return !e || e === "stable" ? "text-text-secondary" : e === "up" && (t ?? 0) >= 0 ? "text-status-success-text" : e === "down" && (t ?? 0) < 0 ? "text-status-error-text" : "text-text-secondary";
18
18
  }
19
19
  function s(e) {
20
20
  return e === "up" ? "↑" : e === "down" ? "↓" : "→";
@@ -159,7 +159,7 @@ var c = n(function({ widget: n, data: c, onClick: l, onDrilldown: u }) {
159
159
  }), /* @__PURE__ */ r("div", {
160
160
  className: "h-1 bg-bg-sunken rounded-full overflow-hidden",
161
161
  children: /* @__PURE__ */ r("div", {
162
- className: `h-full rounded-full ${n.value >= n.target ? "bg-state-success" : "bg-action-primary-bg"}`,
162
+ className: `h-full rounded-full ${n.value >= n.target ? "bg-status-success-bg" : "bg-action-primary-bg"}`,
163
163
  style: { width: `${Math.min(n.value / n.target * 100, 100)}%` }
164
164
  })
165
165
  })]
@@ -1 +1 @@
1
- {"version":3,"file":"KPIComparisonWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx"],"sourcesContent":["/**\n * KPIComparisonWidget Component\n *\n * Displays multiple KPIs in a grid for comparison.\n */\n\nimport { type FC, memo } from 'react';\nimport type { Widget } from '../../../types';\nimport { type ConditionalRule, resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIComparisonWidgetProps {\n widget: Widget;\n data?: KPIComparisonData;\n onClick?: (kpi: KPIItem) => void;\n onDrilldown?: (data: Record<string, unknown>) => void;\n}\n\nexport interface KPIItem {\n id: string;\n label: string;\n value: number;\n previousValue?: number;\n target?: number;\n unit?: string;\n trend?: 'up' | 'down' | 'stable';\n trendValue?: number;\n}\n\nexport interface KPIComparisonData {\n kpis?: KPIItem[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number, unit?: string): string {\n if (unit === '$') {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n }\n if (unit === '%') {\n return `${value.toFixed(1)}%`;\n }\n return new Intl.NumberFormat('en-US', {\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction getTrendColor(trend?: string, trendValue?: number): string {\n if (!trend || trend === 'stable') return 'text-text-secondary';\n if (trend === 'up' && (trendValue ?? 0) >= 0) return 'text-state-success';\n if (trend === 'down' && (trendValue ?? 0) < 0) return 'text-state-error';\n return 'text-text-secondary';\n}\n\nfunction getTrendIcon(trend?: string): string {\n if (trend === 'up') return '↑';\n if (trend === 'down') return '↓';\n return '→';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIComparisonWidget: FC<KPIComparisonWidgetProps> = memo(function KPIComparisonWidget({\n widget,\n data,\n onClick,\n onDrilldown,\n}) {\n const hasDrilldown = !!widget.drilldown?.enabled;\n const config = widget.config as Record<string, unknown> | undefined;\n const conditionalRules = config?.conditionalRules as ConditionalRule[] | undefined;\n\n // Build KPIs array from data\n // Support multiple formats:\n // 1. data.kpis array (standard format)\n // 2. Single value from data.processed (e.g., 125000 from DataSink)\n // 3. data is a number/string directly\n // 4. Nested summary object (e.g., {sales: [...], summary: {totalSales: 123}})\n // 5. Array of objects from DataSink (e.g., [{month: \"Jan\", revenue: 45000}, ...])\n let kpis: KPIItem[] = [];\n\n // Get configured value field from widget config (for DataSink data)\n const valueField = (config?.valueField as string) || (config?.value_field as string) || 'value';\n const labelField = (config?.labelField as string) || (config?.label_field as string) || 'label';\n\n // Helper to convert object with numeric values to KPIs\n const objectToKpis = (obj: Record<string, unknown>): KPIItem[] => {\n const entries = Object.entries(obj);\n return entries\n .filter(([, v]) => typeof v === 'number')\n .map(([key, value]) => ({\n id: key,\n label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: value as number,\n unit:\n key.toLowerCase().includes('sales') ||\n key.toLowerCase().includes('revenue') ||\n key.toLowerCase().includes('value')\n ? '$'\n : undefined,\n }));\n };\n\n // Helper to extract KPI from a single data row using config fields\n const extractKpiFromRow = (row: Record<string, unknown>, index: number): KPIItem | null => {\n // Try to get value from configured field or common field names\n const valueCandidates = [valueField, 'value', 'revenue', 'amount', 'total', 'count', 'sales', 'growth'];\n let value: number | null = null;\n\n for (const field of valueCandidates) {\n if (typeof row[field] === 'number') {\n value = row[field] as number;\n break;\n }\n if (typeof row[field] === 'string' && !isNaN(parseFloat(row[field] as string))) {\n value = parseFloat(row[field] as string);\n break;\n }\n }\n\n if (value === null) {\n // Try to find any numeric field\n const numericEntry = Object.entries(row).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n value = numericEntry[1] as number;\n }\n }\n\n if (value === null) return null;\n\n // Try to get label from configured field or common field names\n const labelCandidates = [labelField, 'label', 'name', 'title', 'month', 'region', 'category'];\n let label = `Item ${index + 1}`;\n\n for (const field of labelCandidates) {\n if (row[field] && typeof row[field] === 'string') {\n label = row[field] as string;\n break;\n }\n }\n\n // Check for growth/trend fields\n const growthValue = row.growth ?? row.change ?? row.trend ?? row.percentChange;\n const trend =\n typeof growthValue === 'number' ? (growthValue > 0 ? 'up' : growthValue < 0 ? 'down' : 'stable') : undefined;\n\n return {\n id: `kpi-${index}`,\n label,\n value,\n unit:\n valueField.toLowerCase().includes('revenue') || valueField.toLowerCase().includes('sales') ? '$' : undefined,\n trend: trend as 'up' | 'down' | 'stable' | undefined,\n trendValue: typeof growthValue === 'number' ? growthValue : undefined,\n };\n };\n\n if (data?.kpis && Array.isArray(data.kpis)) {\n // Standard KPIs array\n kpis = data.kpis;\n } else if (Array.isArray(data) && data.length > 0) {\n // NEW: Handle array of objects from DataSink\n // Extract KPIs from each row, up to 4 items for display\n const maxKpis = 4;\n for (let i = 0; i < Math.min(data.length, maxKpis); i++) {\n const row = data[i] as Record<string, unknown>;\n if (row && typeof row === 'object') {\n const kpi = extractKpiFromRow(row, i);\n if (kpi) kpis.push(kpi);\n }\n }\n\n // If we couldn't extract KPIs from rows, try to aggregate numeric fields\n if (kpis.length === 0 && data.length > 0) {\n const firstRow = data[0] as Record<string, unknown>;\n const numericFields = Object.entries(firstRow).filter(([, v]) => typeof v === 'number');\n\n // Create KPIs by aggregating (summing) each numeric field across all rows\n kpis = numericFields.slice(0, maxKpis).map(([field]) => {\n const sum = data.reduce((acc, row) => {\n const val = (row as Record<string, unknown>)[field];\n return acc + (typeof val === 'number' ? val : 0);\n }, 0);\n\n return {\n id: field,\n label: field.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: sum,\n unit: field.toLowerCase().includes('revenue') || field.toLowerCase().includes('sales') ? '$' : undefined,\n };\n });\n }\n } else if (typeof data === 'number' || typeof data === 'string') {\n // Single value - create a KPI from it using widget config\n const value = typeof data === 'string' ? parseFloat(data) : data;\n if (!isNaN(value)) {\n kpis = [\n {\n id: 'primary',\n label: ((config?.subtitle as string) || widget.title || 'Value').trim(),\n value,\n unit: config?.format === 'currency' ? '$' : config?.format === 'percent' ? '%' : undefined,\n },\n ];\n }\n } else if (data && typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as Record<string, unknown>;\n\n // First, check if there's a nested 'summary' object with numeric values\n if ('summary' in dataObj && dataObj.summary && typeof dataObj.summary === 'object') {\n kpis = objectToKpis(dataObj.summary as Record<string, unknown>);\n }\n\n // If no KPIs from summary, try top-level numeric values\n if (kpis.length === 0) {\n kpis = objectToKpis(dataObj);\n }\n }\n\n if (!kpis.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No KPI data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full p-4 overflow-auto\">\n <div className=\"grid grid-cols-2 gap-3\">\n {kpis.map((kpi) => (\n <div\n key={kpi.id}\n className={`\n p-3 rounded-lg border border-border-default bg-bg-sunken/30\n ${onClick || hasDrilldown ? 'cursor-pointer hover:border-action-primary-bg transition-colors' : ''}\n `}\n onClick={() => {\n // Trigger drilldown with KPI data if enabled\n if (hasDrilldown && onDrilldown) {\n onDrilldown({\n kpiId: kpi.id,\n kpiLabel: kpi.label,\n kpiValue: kpi.value,\n kpiUnit: kpi.unit,\n kpiTrend: kpi.trend,\n kpiTrendValue: kpi.trendValue,\n kpiTarget: kpi.target,\n kpiPreviousValue: kpi.previousValue,\n });\n }\n onClick?.(kpi);\n }}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary truncate mb-1\">{kpi.label}</p>\n\n {/* Value (with optional threshold-based conditional color) */}\n <p\n className={`text-xl font-semibold ${\n conditionalStyleToClassName(\n resolveConditionalStyle(kpi.value, conditionalRules, { [kpi.label]: kpi.value, value: kpi.value })\n ) || 'text-text-primary'\n }`}\n >\n {formatValue(kpi.value, kpi.unit)}\n </p>\n\n {/* Trend */}\n {kpi.trendValue !== undefined && (\n <div className={`flex items-center gap-1 mt-1 text-xs ${getTrendColor(kpi.trend, kpi.trendValue)}`}>\n <span>{getTrendIcon(kpi.trend)}</span>\n <span>\n {kpi.trendValue >= 0 ? '+' : ''}\n {kpi.trendValue.toFixed(1)}%\n </span>\n </div>\n )}\n\n {/* Target progress */}\n {kpi.target !== undefined && (\n <div className=\"mt-2\">\n <div className=\"flex justify-between text-[10px] text-text-tertiary mb-0.5\">\n <span>vs target</span>\n <span>{((kpi.value / kpi.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${kpi.value >= kpi.target ? 'bg-state-success' : 'bg-action-primary-bg'}`}\n style={{ width: `${Math.min((kpi.value / kpi.target) * 100, 100)}%` }}\n />\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIComparisonWidget;\n"],"mappings":"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"}
1
+ {"version":3,"file":"KPIComparisonWidget.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/kpi-comparison/KPIComparisonWidget.tsx"],"sourcesContent":["/**\n * KPIComparisonWidget Component\n *\n * Displays multiple KPIs in a grid for comparison.\n */\n\nimport { type FC, memo } from 'react';\nimport type { Widget } from '../../../types';\nimport { type ConditionalRule, resolveConditionalStyle, conditionalStyleToClassName } from '../utils/conditionalFormat';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface KPIComparisonWidgetProps {\n widget: Widget;\n data?: KPIComparisonData;\n onClick?: (kpi: KPIItem) => void;\n onDrilldown?: (data: Record<string, unknown>) => void;\n}\n\nexport interface KPIItem {\n id: string;\n label: string;\n value: number;\n previousValue?: number;\n target?: number;\n unit?: string;\n trend?: 'up' | 'down' | 'stable';\n trendValue?: number;\n}\n\nexport interface KPIComparisonData {\n kpis?: KPIItem[];\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction formatValue(value: number, unit?: string): string {\n if (unit === '$') {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n }\n if (unit === '%') {\n return `${value.toFixed(1)}%`;\n }\n return new Intl.NumberFormat('en-US', {\n notation: value >= 10000 ? 'compact' : 'standard',\n maximumFractionDigits: 1,\n }).format(value);\n}\n\nfunction getTrendColor(trend?: string, trendValue?: number): string {\n if (!trend || trend === 'stable') return 'text-text-secondary';\n if (trend === 'up' && (trendValue ?? 0) >= 0) return 'text-status-success-text';\n if (trend === 'down' && (trendValue ?? 0) < 0) return 'text-status-error-text';\n return 'text-text-secondary';\n}\n\nfunction getTrendIcon(trend?: string): string {\n if (trend === 'up') return '↑';\n if (trend === 'down') return '↓';\n return '→';\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const KPIComparisonWidget: FC<KPIComparisonWidgetProps> = memo(function KPIComparisonWidget({\n widget,\n data,\n onClick,\n onDrilldown,\n}) {\n const hasDrilldown = !!widget.drilldown?.enabled;\n const config = widget.config as Record<string, unknown> | undefined;\n const conditionalRules = config?.conditionalRules as ConditionalRule[] | undefined;\n\n // Build KPIs array from data\n // Support multiple formats:\n // 1. data.kpis array (standard format)\n // 2. Single value from data.processed (e.g., 125000 from DataSink)\n // 3. data is a number/string directly\n // 4. Nested summary object (e.g., {sales: [...], summary: {totalSales: 123}})\n // 5. Array of objects from DataSink (e.g., [{month: \"Jan\", revenue: 45000}, ...])\n let kpis: KPIItem[] = [];\n\n // Get configured value field from widget config (for DataSink data)\n const valueField = (config?.valueField as string) || (config?.value_field as string) || 'value';\n const labelField = (config?.labelField as string) || (config?.label_field as string) || 'label';\n\n // Helper to convert object with numeric values to KPIs\n const objectToKpis = (obj: Record<string, unknown>): KPIItem[] => {\n const entries = Object.entries(obj);\n return entries\n .filter(([, v]) => typeof v === 'number')\n .map(([key, value]) => ({\n id: key,\n label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: value as number,\n unit:\n key.toLowerCase().includes('sales') ||\n key.toLowerCase().includes('revenue') ||\n key.toLowerCase().includes('value')\n ? '$'\n : undefined,\n }));\n };\n\n // Helper to extract KPI from a single data row using config fields\n const extractKpiFromRow = (row: Record<string, unknown>, index: number): KPIItem | null => {\n // Try to get value from configured field or common field names\n const valueCandidates = [valueField, 'value', 'revenue', 'amount', 'total', 'count', 'sales', 'growth'];\n let value: number | null = null;\n\n for (const field of valueCandidates) {\n if (typeof row[field] === 'number') {\n value = row[field] as number;\n break;\n }\n if (typeof row[field] === 'string' && !isNaN(parseFloat(row[field] as string))) {\n value = parseFloat(row[field] as string);\n break;\n }\n }\n\n if (value === null) {\n // Try to find any numeric field\n const numericEntry = Object.entries(row).find(([, v]) => typeof v === 'number');\n if (numericEntry) {\n value = numericEntry[1] as number;\n }\n }\n\n if (value === null) return null;\n\n // Try to get label from configured field or common field names\n const labelCandidates = [labelField, 'label', 'name', 'title', 'month', 'region', 'category'];\n let label = `Item ${index + 1}`;\n\n for (const field of labelCandidates) {\n if (row[field] && typeof row[field] === 'string') {\n label = row[field] as string;\n break;\n }\n }\n\n // Check for growth/trend fields\n const growthValue = row.growth ?? row.change ?? row.trend ?? row.percentChange;\n const trend =\n typeof growthValue === 'number' ? (growthValue > 0 ? 'up' : growthValue < 0 ? 'down' : 'stable') : undefined;\n\n return {\n id: `kpi-${index}`,\n label,\n value,\n unit:\n valueField.toLowerCase().includes('revenue') || valueField.toLowerCase().includes('sales') ? '$' : undefined,\n trend: trend as 'up' | 'down' | 'stable' | undefined,\n trendValue: typeof growthValue === 'number' ? growthValue : undefined,\n };\n };\n\n if (data?.kpis && Array.isArray(data.kpis)) {\n // Standard KPIs array\n kpis = data.kpis;\n } else if (Array.isArray(data) && data.length > 0) {\n // NEW: Handle array of objects from DataSink\n // Extract KPIs from each row, up to 4 items for display\n const maxKpis = 4;\n for (let i = 0; i < Math.min(data.length, maxKpis); i++) {\n const row = data[i] as Record<string, unknown>;\n if (row && typeof row === 'object') {\n const kpi = extractKpiFromRow(row, i);\n if (kpi) kpis.push(kpi);\n }\n }\n\n // If we couldn't extract KPIs from rows, try to aggregate numeric fields\n if (kpis.length === 0 && data.length > 0) {\n const firstRow = data[0] as Record<string, unknown>;\n const numericFields = Object.entries(firstRow).filter(([, v]) => typeof v === 'number');\n\n // Create KPIs by aggregating (summing) each numeric field across all rows\n kpis = numericFields.slice(0, maxKpis).map(([field]) => {\n const sum = data.reduce((acc, row) => {\n const val = (row as Record<string, unknown>)[field];\n return acc + (typeof val === 'number' ? val : 0);\n }, 0);\n\n return {\n id: field,\n label: field.replace(/([A-Z])/g, ' $1').replace(/^./, (s) => s.toUpperCase()),\n value: sum,\n unit: field.toLowerCase().includes('revenue') || field.toLowerCase().includes('sales') ? '$' : undefined,\n };\n });\n }\n } else if (typeof data === 'number' || typeof data === 'string') {\n // Single value - create a KPI from it using widget config\n const value = typeof data === 'string' ? parseFloat(data) : data;\n if (!isNaN(value)) {\n kpis = [\n {\n id: 'primary',\n label: ((config?.subtitle as string) || widget.title || 'Value').trim(),\n value,\n unit: config?.format === 'currency' ? '$' : config?.format === 'percent' ? '%' : undefined,\n },\n ];\n }\n } else if (data && typeof data === 'object' && !Array.isArray(data)) {\n const dataObj = data as Record<string, unknown>;\n\n // First, check if there's a nested 'summary' object with numeric values\n if ('summary' in dataObj && dataObj.summary && typeof dataObj.summary === 'object') {\n kpis = objectToKpis(dataObj.summary as Record<string, unknown>);\n }\n\n // If no KPIs from summary, try top-level numeric values\n if (kpis.length === 0) {\n kpis = objectToKpis(dataObj);\n }\n }\n\n if (!kpis.length) {\n return (\n <div className=\"flex items-center justify-center h-full text-text-secondary text-sm\">\n <div className=\"text-center\">\n <svg\n className=\"w-12 h-12 mx-auto mb-2 text-text-tertiary\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z\"\n />\n </svg>\n <p>No KPI data</p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"h-full p-4 overflow-auto\">\n <div className=\"grid grid-cols-2 gap-3\">\n {kpis.map((kpi) => (\n <div\n key={kpi.id}\n className={`\n p-3 rounded-lg border border-border-default bg-bg-sunken/30\n ${onClick || hasDrilldown ? 'cursor-pointer hover:border-action-primary-bg transition-colors' : ''}\n `}\n onClick={() => {\n // Trigger drilldown with KPI data if enabled\n if (hasDrilldown && onDrilldown) {\n onDrilldown({\n kpiId: kpi.id,\n kpiLabel: kpi.label,\n kpiValue: kpi.value,\n kpiUnit: kpi.unit,\n kpiTrend: kpi.trend,\n kpiTrendValue: kpi.trendValue,\n kpiTarget: kpi.target,\n kpiPreviousValue: kpi.previousValue,\n });\n }\n onClick?.(kpi);\n }}\n >\n {/* Label */}\n <p className=\"text-xs text-text-secondary truncate mb-1\">{kpi.label}</p>\n\n {/* Value (with optional threshold-based conditional color) */}\n <p\n className={`text-xl font-semibold ${\n conditionalStyleToClassName(\n resolveConditionalStyle(kpi.value, conditionalRules, { [kpi.label]: kpi.value, value: kpi.value })\n ) || 'text-text-primary'\n }`}\n >\n {formatValue(kpi.value, kpi.unit)}\n </p>\n\n {/* Trend */}\n {kpi.trendValue !== undefined && (\n <div className={`flex items-center gap-1 mt-1 text-xs ${getTrendColor(kpi.trend, kpi.trendValue)}`}>\n <span>{getTrendIcon(kpi.trend)}</span>\n <span>\n {kpi.trendValue >= 0 ? '+' : ''}\n {kpi.trendValue.toFixed(1)}%\n </span>\n </div>\n )}\n\n {/* Target progress */}\n {kpi.target !== undefined && (\n <div className=\"mt-2\">\n <div className=\"flex justify-between text-[10px] text-text-tertiary mb-0.5\">\n <span>vs target</span>\n <span>{((kpi.value / kpi.target) * 100).toFixed(0)}%</span>\n </div>\n <div className=\"h-1 bg-bg-sunken rounded-full overflow-hidden\">\n <div\n className={`h-full rounded-full ${kpi.value >= kpi.target ? 'bg-status-success-bg' : 'bg-action-primary-bg'}`}\n style={{ width: `${Math.min((kpi.value / kpi.target) * 100, 100)}%` }}\n />\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n </div>\n );\n});\n\nexport default KPIComparisonWidget;\n"],"mappings":"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"}
@@ -3,9 +3,9 @@ import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
3
  //#region src/bigconsole/components/widgets/list/ListItem.tsx
4
4
  function r(e) {
5
5
  switch (e?.color) {
6
- case "success": return "bg-state-success/10 text-state-success";
7
- case "warning": return "bg-state-warning/10 text-state-warning";
8
- case "error": return "bg-state-error/10 text-state-error";
6
+ case "success": return "bg-status-success-bg/10 text-status-success-text";
7
+ case "warning": return "bg-status-warning-bg/10 text-status-warning-text";
8
+ case "error": return "bg-status-error-bg/10 text-status-error-text";
9
9
  case "info": return "bg-action-primary-bg/10 text-action-primary-bg";
10
10
  default: return "bg-bg-sunken text-text-secondary";
11
11
  }
@@ -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-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"}
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-status-success-bg/10 text-status-success-text';\n case 'warning':\n return 'bg-status-warning-bg/10 text-status-warning-text';\n case 'error':\n return 'bg-status-error-bg/10 text-status-error-text';\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"}
@@ -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-sunken text-text-secondary" : c ? "bg-state-success/10 text-state-success" : "bg-state-error/10 text-state-error"}
12
+ ${c === null ? "bg-bg-sunken text-text-secondary" : c ? "bg-status-success-bg/10 text-status-success-text" : "bg-status-error-bg/10 text-status-error-text"}
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-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"}
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-status-success-bg/10 text-status-success-text' : 'bg-status-error-bg/10 text-status-error-text';\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,qDAAqD,+CAoBhD;;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"}
@@ -155,7 +155,7 @@ var g = c(function({ widget: c, data: g, onClick: _, onDrilldown: v }) {
155
155
  }), /* @__PURE__ */ d("div", {
156
156
  className: "h-1.5 bg-bg-sunken rounded-full overflow-hidden",
157
157
  children: /* @__PURE__ */ d("div", {
158
- className: `h-full rounded-full transition-all ${b.value >= b.target ? "bg-state-success" : "bg-action-primary-bg"}`,
158
+ className: `h-full rounded-full transition-all ${b.value >= b.target ? "bg-status-success-bg" : "bg-action-primary-bg"}`,
159
159
  style: { width: `${Math.min(b.value / b.target * 100, 100)}%` }
160
160
  })
161
161
  })]
@@ -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 // A parser can return a bare scalar (e.g. `. | length` -> 127). Handle that\n // up front: the `in` operator below throws \"Cannot use 'in' operator to\n // search for 'value' in 127\" when rawData is a primitive, which the widget\n // error boundary surfaces as \"Failed to render\" (BOFF-4110).\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // If data already has 'value' property, use it directly. Guard the `in`\n // operator with an object check — it only accepts an object on the RHS.\n if (typeof rawData === 'object' && '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 | 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 | 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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"}
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 // A parser can return a bare scalar (e.g. `. | length` -> 127). Handle that\n // up front: the `in` operator below throws \"Cannot use 'in' operator to\n // search for 'value' in 127\" when rawData is a primitive, which the widget\n // error boundary surfaces as \"Failed to render\" (BOFF-4110).\n if (typeof rawData === 'number' || typeof rawData === 'string') {\n return { value: rawData };\n }\n\n // If data already has 'value' property, use it directly. Guard the `in`\n // operator with an object check — it only accepts an object on the RHS.\n if (typeof rawData === 'object' && '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 | 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 | 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-status-success-bg' : '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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"}
@@ -7,16 +7,16 @@ var i = {
7
7
  fill: "fill-action-primary-bg/20"
8
8
  },
9
9
  success: {
10
- stroke: "stroke-state-success",
11
- fill: "fill-state-success/20"
10
+ stroke: "stroke-status-success-bg",
11
+ fill: "fill-status-success-bg/20"
12
12
  },
13
13
  error: {
14
- stroke: "stroke-state-error",
15
- fill: "fill-state-error/20"
14
+ stroke: "stroke-status-error-bg",
15
+ fill: "fill-status-error-bg/20"
16
16
  },
17
17
  warning: {
18
- stroke: "stroke-state-warning",
19
- fill: "fill-state-warning/20"
18
+ stroke: "stroke-status-warning-bg",
19
+ fill: "fill-status-warning-bg/20"
20
20
  },
21
21
  muted: {
22
22
  stroke: "stroke-text-secondary",
@@ -1 +1 @@
1
- {"version":3,"file":"SparklineChart.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/SparklineChart.tsx"],"sourcesContent":["/**\n * SparklineChart Component\n *\n * Mini trend chart for metric cards.\n */\n\nimport { type FC, memo, useMemo } from 'react';\n\nexport interface SparklineDataPoint {\n value: number;\n timestamp?: string;\n}\n\nexport interface SparklineChartProps {\n /** Data points for the sparkline */\n data: SparklineDataPoint[];\n /** Width of the chart */\n width?: number;\n /** Height of the chart */\n height?: number;\n /** Line color (uses semantic token) */\n color?: 'primary' | 'success' | 'error' | 'warning' | 'muted';\n /** Whether to show the fill area */\n showFill?: boolean;\n /** Stroke width */\n strokeWidth?: number;\n}\n\nconst COLOR_CLASSES = {\n primary: {\n stroke: 'stroke-action-primary-bg',\n fill: 'fill-action-primary-bg/20',\n },\n success: {\n stroke: 'stroke-state-success',\n fill: 'fill-state-success/20',\n },\n error: {\n stroke: 'stroke-state-error',\n fill: 'fill-state-error/20',\n },\n warning: {\n stroke: 'stroke-state-warning',\n fill: 'fill-state-warning/20',\n },\n muted: {\n stroke: 'stroke-text-secondary',\n fill: 'fill-text-secondary/10',\n },\n};\n\nexport const SparklineChart: FC<SparklineChartProps> = memo(function SparklineChart({\n data,\n width = 100,\n height = 32,\n color = 'primary',\n showFill = true,\n strokeWidth = 2,\n}) {\n const pathData = useMemo(() => {\n if (!data.length) return { line: '', area: '' };\n\n const values = data.map((d) => d.value);\n const min = Math.min(...values);\n const max = Math.max(...values);\n const range = max - min || 1;\n\n const padding = strokeWidth;\n const chartWidth = width - padding * 2;\n const chartHeight = height - padding * 2;\n\n const points = data.map((d, i) => {\n const x = padding + (i / (data.length - 1)) * chartWidth;\n const y = padding + chartHeight - ((d.value - min) / range) * chartHeight;\n return { x, y };\n });\n\n // Create line path\n const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');\n\n // Create area path (closed shape for fill)\n const areaPath = `\n ${linePath}\n L ${points[points.length - 1].x} ${height - padding}\n L ${padding} ${height - padding}\n Z\n `;\n\n return { line: linePath, area: areaPath };\n }, [data, width, height, strokeWidth]);\n\n if (!data.length) {\n return (\n <div className=\"flex items-center justify-center text-text-secondary\" style={{ width, height }}>\n <span className=\"text-xs\">No data</span>\n </div>\n );\n }\n\n const colors = COLOR_CLASSES[color];\n\n return (\n <svg width={width} height={height} className=\"overflow-visible\" role=\"img\" aria-label=\"Sparkline chart\">\n {/* Gradient fill */}\n {showFill && <path d={pathData.area} className={colors.fill} strokeWidth={0} />}\n\n {/* Line */}\n <path\n d={pathData.line}\n className={colors.stroke}\n fill=\"none\"\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n\n {/* End dot */}\n {data.length > 0 && (\n <circle\n cx={width - strokeWidth}\n cy={\n strokeWidth +\n (height - strokeWidth * 2) -\n ((data[data.length - 1].value - Math.min(...data.map((d) => d.value))) /\n (Math.max(...data.map((d) => d.value)) - Math.min(...data.map((d) => d.value)) || 1)) *\n (height - strokeWidth * 2)\n }\n r={strokeWidth + 1}\n className={`${colors.stroke.replace('stroke-', 'fill-')}`}\n />\n )}\n </svg>\n );\n});\n\nexport default SparklineChart;\n"],"mappings":";;;AA4BA,IAAM,IAAgB;CACpB,SAAS;EACP,QAAQ;EACR,MAAM;EACP;CACD,SAAS;EACP,QAAQ;EACR,MAAM;EACP;CACD,OAAO;EACL,QAAQ;EACR,MAAM;EACP;CACD,SAAS;EACP,QAAQ;EACR,MAAM;EACP;CACD,OAAO;EACL,QAAQ;EACR,MAAM;EACP;CACF,EAEY,IAA0C,EAAK,SAAwB,EAClF,SACA,WAAQ,KACR,YAAS,IACT,WAAQ,WACR,cAAW,IACX,iBAAc,KACb;CACD,IAAM,IAAW,QAAc;AAC7B,MAAI,CAAC,EAAK,OAAQ,QAAO;GAAE,MAAM;GAAI,MAAM;GAAI;EAE/C,IAAM,IAAS,EAAK,KAAK,MAAM,EAAE,MAAM,EACjC,IAAM,KAAK,IAAI,GAAG,EAAO,EAEzB,IADM,KAAK,IAAI,GAAG,EAAO,GACX,KAAO,GAErB,IAAU,GACV,IAAa,IAAQ,IAAU,GAC/B,IAAc,IAAS,IAAU,GAEjC,IAAS,EAAK,KAAK,GAAG,OAGnB;GAAE,GAFC,IAAW,KAAK,EAAK,SAAS,KAAM;GAElC,GADF,IAAU,KAAgB,EAAE,QAAQ,KAAO,IAAS;GAC/C,EACf,EAGI,IAAW,EAAO,KAAK,GAAG,MAAM,GAAG,MAAM,IAAI,MAAM,IAAI,GAAG,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,IAAI;AAUvF,SAAO;GAAE,MAAM;GAAU,MAPR;UACX,EAAS;YACP,EAAO,EAAO,SAAS,GAAG,EAAE,GAAG,IAAS,EAAQ;YAChD,EAAQ,GAAG,IAAS,EAAQ;;;GAIK;IACxC;EAAC;EAAM;EAAO;EAAQ;EAAY,CAAC;AAEtC,KAAI,CAAC,EAAK,OACR,QACE,kBAAC,OAAD;EAAK,WAAU;EAAuD,OAAO;GAAE;GAAO;GAAQ;YAC5F,kBAAC,QAAD;GAAM,WAAU;aAAU;GAAc,CAAA;EACpC,CAAA;CAIV,IAAM,IAAS,EAAc;AAE7B,QACE,kBAAC,OAAD;EAAY;EAAe;EAAQ,WAAU;EAAmB,MAAK;EAAM,cAAW;YAAtF;GAEG,KAAY,kBAAC,QAAD;IAAM,GAAG,EAAS;IAAM,WAAW,EAAO;IAAM,aAAa;IAAK,CAAA;GAG/E,kBAAC,QAAD;IACE,GAAG,EAAS;IACZ,WAAW,EAAO;IAClB,MAAK;IACQ;IACb,eAAc;IACd,gBAAe;IACf,CAAA;GAGD,EAAK,SAAS,KACb,kBAAC,UAAD;IACE,IAAI,IAAQ;IACZ,IACE,KACC,IAAS,IAAc,MACtB,EAAK,EAAK,SAAS,GAAG,QAAQ,KAAK,IAAI,GAAG,EAAK,KAAK,MAAM,EAAE,MAAM,CAAC,KAClE,KAAK,IAAI,GAAG,EAAK,KAAK,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,IAAI,GAAG,EAAK,KAAK,MAAM,EAAE,MAAM,CAAC,IAAI,MACjF,IAAS,IAAc;IAE5B,GAAG,IAAc;IACjB,WAAW,GAAG,EAAO,OAAO,QAAQ,WAAW,QAAQ;IACvD,CAAA;GAEA;;EAER"}
1
+ {"version":3,"file":"SparklineChart.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/SparklineChart.tsx"],"sourcesContent":["/**\n * SparklineChart Component\n *\n * Mini trend chart for metric cards.\n */\n\nimport { type FC, memo, useMemo } from 'react';\n\nexport interface SparklineDataPoint {\n value: number;\n timestamp?: string;\n}\n\nexport interface SparklineChartProps {\n /** Data points for the sparkline */\n data: SparklineDataPoint[];\n /** Width of the chart */\n width?: number;\n /** Height of the chart */\n height?: number;\n /** Line color (uses semantic token) */\n color?: 'primary' | 'success' | 'error' | 'warning' | 'muted';\n /** Whether to show the fill area */\n showFill?: boolean;\n /** Stroke width */\n strokeWidth?: number;\n}\n\nconst COLOR_CLASSES = {\n primary: {\n stroke: 'stroke-action-primary-bg',\n fill: 'fill-action-primary-bg/20',\n },\n success: {\n stroke: 'stroke-status-success-bg',\n fill: 'fill-status-success-bg/20',\n },\n error: {\n stroke: 'stroke-status-error-bg',\n fill: 'fill-status-error-bg/20',\n },\n warning: {\n stroke: 'stroke-status-warning-bg',\n fill: 'fill-status-warning-bg/20',\n },\n muted: {\n stroke: 'stroke-text-secondary',\n fill: 'fill-text-secondary/10',\n },\n};\n\nexport const SparklineChart: FC<SparklineChartProps> = memo(function SparklineChart({\n data,\n width = 100,\n height = 32,\n color = 'primary',\n showFill = true,\n strokeWidth = 2,\n}) {\n const pathData = useMemo(() => {\n if (!data.length) return { line: '', area: '' };\n\n const values = data.map((d) => d.value);\n const min = Math.min(...values);\n const max = Math.max(...values);\n const range = max - min || 1;\n\n const padding = strokeWidth;\n const chartWidth = width - padding * 2;\n const chartHeight = height - padding * 2;\n\n const points = data.map((d, i) => {\n const x = padding + (i / (data.length - 1)) * chartWidth;\n const y = padding + chartHeight - ((d.value - min) / range) * chartHeight;\n return { x, y };\n });\n\n // Create line path\n const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');\n\n // Create area path (closed shape for fill)\n const areaPath = `\n ${linePath}\n L ${points[points.length - 1].x} ${height - padding}\n L ${padding} ${height - padding}\n Z\n `;\n\n return { line: linePath, area: areaPath };\n }, [data, width, height, strokeWidth]);\n\n if (!data.length) {\n return (\n <div className=\"flex items-center justify-center text-text-secondary\" style={{ width, height }}>\n <span className=\"text-xs\">No data</span>\n </div>\n );\n }\n\n const colors = COLOR_CLASSES[color];\n\n return (\n <svg width={width} height={height} className=\"overflow-visible\" role=\"img\" aria-label=\"Sparkline chart\">\n {/* Gradient fill */}\n {showFill && <path d={pathData.area} className={colors.fill} strokeWidth={0} />}\n\n {/* Line */}\n <path\n d={pathData.line}\n className={colors.stroke}\n fill=\"none\"\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n\n {/* End dot */}\n {data.length > 0 && (\n <circle\n cx={width - strokeWidth}\n cy={\n strokeWidth +\n (height - strokeWidth * 2) -\n ((data[data.length - 1].value - Math.min(...data.map((d) => d.value))) /\n (Math.max(...data.map((d) => d.value)) - Math.min(...data.map((d) => d.value)) || 1)) *\n (height - strokeWidth * 2)\n }\n r={strokeWidth + 1}\n className={`${colors.stroke.replace('stroke-', 'fill-')}`}\n />\n )}\n </svg>\n );\n});\n\nexport default SparklineChart;\n"],"mappings":";;;AA4BA,IAAM,IAAgB;CACpB,SAAS;EACP,QAAQ;EACR,MAAM;EACP;CACD,SAAS;EACP,QAAQ;EACR,MAAM;EACP;CACD,OAAO;EACL,QAAQ;EACR,MAAM;EACP;CACD,SAAS;EACP,QAAQ;EACR,MAAM;EACP;CACD,OAAO;EACL,QAAQ;EACR,MAAM;EACP;CACF,EAEY,IAA0C,EAAK,SAAwB,EAClF,SACA,WAAQ,KACR,YAAS,IACT,WAAQ,WACR,cAAW,IACX,iBAAc,KACb;CACD,IAAM,IAAW,QAAc;AAC7B,MAAI,CAAC,EAAK,OAAQ,QAAO;GAAE,MAAM;GAAI,MAAM;GAAI;EAE/C,IAAM,IAAS,EAAK,KAAK,MAAM,EAAE,MAAM,EACjC,IAAM,KAAK,IAAI,GAAG,EAAO,EAEzB,IADM,KAAK,IAAI,GAAG,EAAO,GACX,KAAO,GAErB,IAAU,GACV,IAAa,IAAQ,IAAU,GAC/B,IAAc,IAAS,IAAU,GAEjC,IAAS,EAAK,KAAK,GAAG,OAGnB;GAAE,GAFC,IAAW,KAAK,EAAK,SAAS,KAAM;GAElC,GADF,IAAU,KAAgB,EAAE,QAAQ,KAAO,IAAS;GAC/C,EACf,EAGI,IAAW,EAAO,KAAK,GAAG,MAAM,GAAG,MAAM,IAAI,MAAM,IAAI,GAAG,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,IAAI;AAUvF,SAAO;GAAE,MAAM;GAAU,MAPR;UACX,EAAS;YACP,EAAO,EAAO,SAAS,GAAG,EAAE,GAAG,IAAS,EAAQ;YAChD,EAAQ,GAAG,IAAS,EAAQ;;;GAIK;IACxC;EAAC;EAAM;EAAO;EAAQ;EAAY,CAAC;AAEtC,KAAI,CAAC,EAAK,OACR,QACE,kBAAC,OAAD;EAAK,WAAU;EAAuD,OAAO;GAAE;GAAO;GAAQ;YAC5F,kBAAC,QAAD;GAAM,WAAU;aAAU;GAAc,CAAA;EACpC,CAAA;CAIV,IAAM,IAAS,EAAc;AAE7B,QACE,kBAAC,OAAD;EAAY;EAAe;EAAQ,WAAU;EAAmB,MAAK;EAAM,cAAW;YAAtF;GAEG,KAAY,kBAAC,QAAD;IAAM,GAAG,EAAS;IAAM,WAAW,EAAO;IAAM,aAAa;IAAK,CAAA;GAG/E,kBAAC,QAAD;IACE,GAAG,EAAS;IACZ,WAAW,EAAO;IAClB,MAAK;IACQ;IACb,eAAc;IACd,gBAAe;IACf,CAAA;GAGD,EAAK,SAAS,KACb,kBAAC,UAAD;IACE,IAAI,IAAQ;IACZ,IACE,KACC,IAAS,IAAc,MACtB,EAAK,EAAK,SAAS,GAAG,QAAQ,KAAK,IAAI,GAAG,EAAK,KAAK,MAAM,EAAE,MAAM,CAAC,KAClE,KAAK,IAAI,GAAG,EAAK,KAAK,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,IAAI,GAAG,EAAK,KAAK,MAAM,EAAE,MAAM,CAAC,IAAI,MACjF,IAAS,IAAc;IAE5B,GAAG,IAAc;IACjB,WAAW,GAAG,EAAO,OAAO,QAAQ,WAAW,QAAQ;IACvD,CAAA;GAEA;;EAER"}
@@ -9,7 +9,7 @@ var n = {
9
9
  let a = () => {
10
10
  if (e === "flat") return "text-text-secondary";
11
11
  let t = e === "up";
12
- return (i ? t : !t) ? "text-state-success" : "text-state-error";
12
+ return (i ? t : !t) ? "text-status-success-text" : "text-status-error-text";
13
13
  }, o = n[r], s = a();
14
14
  return e === "flat" ? /* @__PURE__ */ t("svg", {
15
15
  className: `${o} ${s}`,
@@ -1 +1 @@
1
- {"version":3,"file":"TrendIndicator.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/TrendIndicator.tsx"],"sourcesContent":["/**\n * TrendIndicator Component\n *\n * Displays up/down/flat trend arrow with color coding.\n */\n\nimport { type FC, memo } from 'react';\n\nexport type TrendDirection = 'up' | 'down' | 'flat';\n\nexport interface TrendIndicatorProps {\n /** Direction of the trend */\n direction: TrendDirection;\n /** Size variant */\n size?: 'sm' | 'md' | 'lg';\n /** Whether positive trend is good (default true for metrics like revenue) */\n positiveIsGood?: boolean;\n}\n\nconst SIZE_CLASSES = {\n sm: 'w-3 h-3',\n md: 'w-4 h-4',\n lg: 'w-5 h-5',\n};\n\nexport const TrendIndicator: FC<TrendIndicatorProps> = memo(function TrendIndicator({\n direction,\n size = 'md',\n positiveIsGood = true,\n}) {\n // Determine color based on direction and whether positive is good\n const getColorClass = (): string => {\n if (direction === 'flat') {\n return 'text-text-secondary';\n }\n\n const isPositive = direction === 'up';\n const isGood = positiveIsGood ? isPositive : !isPositive;\n\n return isGood ? 'text-state-success' : 'text-state-error';\n };\n\n const sizeClass = SIZE_CLASSES[size];\n const colorClass = getColorClass();\n\n if (direction === 'flat') {\n return (\n <svg\n className={`${sizeClass} ${colorClass}`}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-label=\"No change\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 12h14\" />\n </svg>\n );\n }\n\n if (direction === 'up') {\n return (\n <svg\n className={`${sizeClass} ${colorClass}`}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-label=\"Trending up\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 15l7-7 7 7\" />\n </svg>\n );\n }\n\n return (\n <svg\n className={`${sizeClass} ${colorClass}`}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-label=\"Trending down\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n );\n});\n\nexport default TrendIndicator;\n"],"mappings":";;;AAmBA,IAAM,IAAe;CACnB,IAAI;CACJ,IAAI;CACJ,IAAI;CACL,EAEY,IAA0C,EAAK,SAAwB,EAClF,cACA,UAAO,MACP,oBAAiB,MAChB;CAED,IAAM,UAA8B;AAClC,MAAI,MAAc,OAChB,QAAO;EAGT,IAAM,IAAa,MAAc;AAGjC,UAFe,IAAiB,IAAa,CAAC,KAE9B,uBAAuB;IAGnC,IAAY,EAAa,IACzB,IAAa,GAAe;AA8BlC,QA5BI,MAAc,SAEd,kBAAC,OAAD;EACE,WAAW,GAAG,EAAU,GAAG;EAC3B,MAAK;EACL,QAAO;EACP,SAAQ;EACR,cAAW;YAEX,kBAAC,QAAD;GAAM,eAAc;GAAQ,gBAAe;GAAQ,aAAa;GAAG,GAAE;GAAa,CAAA;EAC9E,CAAA,GAIN,MAAc,OAEd,kBAAC,OAAD;EACE,WAAW,GAAG,EAAU,GAAG;EAC3B,MAAK;EACL,QAAO;EACP,SAAQ;EACR,cAAW;YAEX,kBAAC,QAAD;GAAM,eAAc;GAAQ,gBAAe;GAAQ,aAAa;GAAG,GAAE;GAAkB,CAAA;EACnF,CAAA,GAKR,kBAAC,OAAD;EACE,WAAW,GAAG,EAAU,GAAG;EAC3B,MAAK;EACL,QAAO;EACP,SAAQ;EACR,cAAW;YAEX,kBAAC,QAAD;GAAM,eAAc;GAAQ,gBAAe;GAAQ,aAAa;GAAG,GAAE;GAAmB,CAAA;EACpF,CAAA;EAER"}
1
+ {"version":3,"file":"TrendIndicator.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/metric-card/TrendIndicator.tsx"],"sourcesContent":["/**\n * TrendIndicator Component\n *\n * Displays up/down/flat trend arrow with color coding.\n */\n\nimport { type FC, memo } from 'react';\n\nexport type TrendDirection = 'up' | 'down' | 'flat';\n\nexport interface TrendIndicatorProps {\n /** Direction of the trend */\n direction: TrendDirection;\n /** Size variant */\n size?: 'sm' | 'md' | 'lg';\n /** Whether positive trend is good (default true for metrics like revenue) */\n positiveIsGood?: boolean;\n}\n\nconst SIZE_CLASSES = {\n sm: 'w-3 h-3',\n md: 'w-4 h-4',\n lg: 'w-5 h-5',\n};\n\nexport const TrendIndicator: FC<TrendIndicatorProps> = memo(function TrendIndicator({\n direction,\n size = 'md',\n positiveIsGood = true,\n}) {\n // Determine color based on direction and whether positive is good\n const getColorClass = (): string => {\n if (direction === 'flat') {\n return 'text-text-secondary';\n }\n\n const isPositive = direction === 'up';\n const isGood = positiveIsGood ? isPositive : !isPositive;\n\n return isGood ? 'text-status-success-text' : 'text-status-error-text';\n };\n\n const sizeClass = SIZE_CLASSES[size];\n const colorClass = getColorClass();\n\n if (direction === 'flat') {\n return (\n <svg\n className={`${sizeClass} ${colorClass}`}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-label=\"No change\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 12h14\" />\n </svg>\n );\n }\n\n if (direction === 'up') {\n return (\n <svg\n className={`${sizeClass} ${colorClass}`}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-label=\"Trending up\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 15l7-7 7 7\" />\n </svg>\n );\n }\n\n return (\n <svg\n className={`${sizeClass} ${colorClass}`}\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-label=\"Trending down\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M19 9l-7 7-7-7\" />\n </svg>\n );\n});\n\nexport default TrendIndicator;\n"],"mappings":";;;AAmBA,IAAM,IAAe;CACnB,IAAI;CACJ,IAAI;CACJ,IAAI;CACL,EAEY,IAA0C,EAAK,SAAwB,EAClF,cACA,UAAO,MACP,oBAAiB,MAChB;CAED,IAAM,UAA8B;AAClC,MAAI,MAAc,OAChB,QAAO;EAGT,IAAM,IAAa,MAAc;AAGjC,UAFe,IAAiB,IAAa,CAAC,KAE9B,6BAA6B;IAGzC,IAAY,EAAa,IACzB,IAAa,GAAe;AA8BlC,QA5BI,MAAc,SAEd,kBAAC,OAAD;EACE,WAAW,GAAG,EAAU,GAAG;EAC3B,MAAK;EACL,QAAO;EACP,SAAQ;EACR,cAAW;YAEX,kBAAC,QAAD;GAAM,eAAc;GAAQ,gBAAe;GAAQ,aAAa;GAAG,GAAE;GAAa,CAAA;EAC9E,CAAA,GAIN,MAAc,OAEd,kBAAC,OAAD;EACE,WAAW,GAAG,EAAU,GAAG;EAC3B,MAAK;EACL,QAAO;EACP,SAAQ;EACR,cAAW;YAEX,kBAAC,QAAD;GAAM,eAAc;GAAQ,gBAAe;GAAQ,aAAa;GAAG,GAAE;GAAkB,CAAA;EACnF,CAAA,GAKR,kBAAC,OAAD;EACE,WAAW,GAAG,EAAU,GAAG;EAC3B,MAAK;EACL,QAAO;EACP,SAAQ;EACR,cAAW;YAEX,kBAAC,QAAD;GAAM,eAAc;GAAQ,gBAAe;GAAQ,aAAa;GAAG,GAAE;GAAmB,CAAA;EACpF,CAAA;EAER"}
@@ -2,11 +2,11 @@ import { memo as e, useMemo as t } from "react";
2
2
  import { jsx as n, jsxs as r } from "react/jsx-runtime";
3
3
  //#region src/bigconsole/components/widgets/progress/ProgressBar.tsx
4
4
  function i(e, t) {
5
- if (t === "dynamic") return e >= 75 ? "bg-state-success" : e >= 50 ? "bg-state-warning" : "bg-state-error";
5
+ if (t === "dynamic") return e >= 75 ? "bg-status-success-bg" : e >= 50 ? "bg-status-warning-bg" : "bg-status-error-bg";
6
6
  switch (t) {
7
- case "success": return "bg-state-success";
8
- case "warning": return "bg-state-warning";
9
- case "error": return "bg-state-error";
7
+ case "success": return "bg-status-success-bg";
8
+ case "warning": return "bg-status-warning-bg";
9
+ case "error": return "bg-status-error-bg";
10
10
  default: return "bg-action-primary-bg";
11
11
  }
12
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ProgressBar.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/progress/ProgressBar.tsx"],"sourcesContent":["/**\n * ProgressBar Component\n *\n * Horizontal progress bar with optional milestones and target.\n */\n\nimport { type FC, memo, useMemo } from 'react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface Milestone {\n /** Position as percentage (0-100) */\n position: number;\n /** Optional label */\n label?: string;\n /** Whether milestone is reached */\n reached?: boolean;\n}\n\nexport interface ProgressBarProps {\n /** Current progress value (0-100) */\n value: number;\n /** Target value to show as marker */\n target?: number;\n /** Milestones to display */\n milestones?: Milestone[];\n /** Height of the bar */\n height?: number;\n /** Color based on progress */\n color?: 'default' | 'success' | 'warning' | 'error' | 'dynamic';\n /** Whether to show percentage label */\n showPercentage?: boolean;\n /** Whether to animate */\n animated?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getProgressColor(value: number, color: ProgressBarProps['color']): string {\n if (color === 'dynamic') {\n if (value >= 75) return 'bg-state-success';\n if (value >= 50) return 'bg-state-warning';\n return 'bg-state-error';\n }\n\n switch (color) {\n case 'success':\n return 'bg-state-success';\n case 'warning':\n return 'bg-state-warning';\n case 'error':\n return 'bg-state-error';\n case 'default':\n default:\n return 'bg-action-primary-bg';\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const ProgressBar: FC<ProgressBarProps> = memo(function ProgressBar({\n value,\n target,\n milestones = [],\n height = 8,\n color = 'default',\n showPercentage = false,\n animated = true,\n}) {\n const clampedValue = useMemo(() => Math.max(0, Math.min(100, value)), [value]);\n\n const progressColor = getProgressColor(clampedValue, color);\n\n return (\n <div className=\"w-full\">\n {/* Progress bar container */}\n <div className=\"relative w-full rounded-full bg-bg-sunken overflow-visible\" style={{ height }}>\n {/* Progress fill */}\n <div\n className={`\n absolute inset-y-0 left-0 rounded-full\n ${progressColor}\n ${animated ? 'transition-all duration-500 ease-out' : ''}\n `}\n style={{ width: `${clampedValue}%` }}\n role=\"progressbar\"\n aria-valuenow={clampedValue}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n\n {/* Target marker */}\n {target !== undefined && target >= 0 && target <= 100 && (\n <div\n className=\"absolute top-1/2 -translate-y-1/2 w-0.5 h-[150%] bg-text-primary z-10\"\n style={{ left: `${target}%` }}\n title={`Target: ${target}%`}\n >\n {/* Target flag */}\n <div className=\"absolute -top-3 left-1/2 -translate-x-1/2\">\n <svg className=\"w-3 h-3 text-text-primary\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M5 21V3h14l-3 9 3 9H5z\" />\n </svg>\n </div>\n </div>\n )}\n\n {/* Milestones */}\n {milestones.map((milestone, index) => (\n <div\n key={index}\n className={`\n absolute top-1/2 -translate-y-1/2\n w-3 h-3 rounded-full border-2\n ${\n milestone.reached || clampedValue >= milestone.position\n ? 'bg-bg-surface border-action-primary-bg'\n : 'bg-bg-sunken border-border-default'\n }\n z-10\n `}\n style={{ left: `${milestone.position}%`, transform: 'translate(-50%, -50%)' }}\n title={milestone.label || `${milestone.position}%`}\n />\n ))}\n </div>\n\n {/* Milestone labels */}\n {milestones.some((m) => m.label) && (\n <div className=\"relative mt-2\">\n {milestones.map((milestone, index) =>\n milestone.label ? (\n <span\n key={index}\n className=\"absolute text-xs text-text-secondary -translate-x-1/2 whitespace-nowrap\"\n style={{ left: `${milestone.position}%` }}\n >\n {milestone.label}\n </span>\n ) : null\n )}\n </div>\n )}\n\n {/* Percentage label */}\n {showPercentage && (\n <div className=\"flex justify-between items-center mt-1\">\n <span className=\"text-xs text-text-secondary\">0%</span>\n <span className=\"text-sm font-medium text-text-primary\">{clampedValue.toFixed(0)}%</span>\n <span className=\"text-xs text-text-secondary\">100%</span>\n </div>\n )}\n </div>\n );\n});\n\nexport default ProgressBar;\n"],"mappings":";;;AA0CA,SAAS,EAAiB,GAAe,GAA0C;AACjF,KAAI,MAAU,UAGZ,QAFI,KAAS,KAAW,qBACpB,KAAS,KAAW,qBACjB;AAGT,SAAQ,GAAR;EACE,KAAK,UACH,QAAO;EACT,KAAK,UACH,QAAO;EACT,KAAK,QACH,QAAO;EAET,QACE,QAAO;;;AAQb,IAAa,IAAoC,EAAK,SAAqB,EACzE,UACA,WACA,gBAAa,EAAE,EACf,YAAS,GACT,WAAQ,WACR,oBAAiB,IACjB,cAAW,MACV;CACD,IAAM,IAAe,QAAc,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,EAAM,CAAC,EAAE,CAAC,EAAM,CAAC,EAExE,IAAgB,EAAiB,GAAc,EAAM;AAE3D,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;IAA6D,OAAO,EAAE,WAAQ;cAA7F;KAEE,kBAAC,OAAD;MACE,WAAW;;cAEP,EAAc;cACd,IAAW,yCAAyC,GAAG;;MAE3D,OAAO,EAAE,OAAO,GAAG,EAAa,IAAI;MACpC,MAAK;MACL,iBAAe;MACf,iBAAe;MACf,iBAAe;MACf,CAAA;KAGD,MAAW,KAAA,KAAa,KAAU,KAAK,KAAU,OAChD,kBAAC,OAAD;MACE,WAAU;MACV,OAAO,EAAE,MAAM,GAAG,EAAO,IAAI;MAC7B,OAAO,WAAW,EAAO;gBAGzB,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,OAAD;QAAK,WAAU;QAA4B,MAAK;QAAe,SAAQ;kBACrE,kBAAC,QAAD,EAAM,GAAE,0BAA2B,CAAA;QAC/B,CAAA;OACF,CAAA;MACF,CAAA;KAIP,EAAW,KAAK,GAAW,MAC1B,kBAAC,OAAD;MAEE,WAAW;;;gBAIP,EAAU,WAAW,KAAgB,EAAU,WAC3C,2CACA,qCACL;;;MAGH,OAAO;OAAE,MAAM,GAAG,EAAU,SAAS;OAAI,WAAW;OAAyB;MAC7E,OAAO,EAAU,SAAS,GAAG,EAAU,SAAS;MAChD,EAbK,EAaL,CACF;KACE;;GAGL,EAAW,MAAM,MAAM,EAAE,MAAM,IAC9B,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAW,KAAK,GAAW,MAC1B,EAAU,QACR,kBAAC,QAAD;KAEE,WAAU;KACV,OAAO,EAAE,MAAM,GAAG,EAAU,SAAS,IAAI;eAExC,EAAU;KACN,EALA,EAKA,GACL,KACL;IACG,CAAA;GAIP,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD;MAAM,WAAU;gBAA8B;MAAS,CAAA;KACvD,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CAAyD,EAAa,QAAQ,EAAE,EAAC,IAAQ;;KACzF,kBAAC,QAAD;MAAM,WAAU;gBAA8B;MAAW,CAAA;KACrD;;GAEJ;;EAER"}
1
+ {"version":3,"file":"ProgressBar.js","names":[],"sources":["../../../../../src/bigconsole/components/widgets/progress/ProgressBar.tsx"],"sourcesContent":["/**\n * ProgressBar Component\n *\n * Horizontal progress bar with optional milestones and target.\n */\n\nimport { type FC, memo, useMemo } from 'react';\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface Milestone {\n /** Position as percentage (0-100) */\n position: number;\n /** Optional label */\n label?: string;\n /** Whether milestone is reached */\n reached?: boolean;\n}\n\nexport interface ProgressBarProps {\n /** Current progress value (0-100) */\n value: number;\n /** Target value to show as marker */\n target?: number;\n /** Milestones to display */\n milestones?: Milestone[];\n /** Height of the bar */\n height?: number;\n /** Color based on progress */\n color?: 'default' | 'success' | 'warning' | 'error' | 'dynamic';\n /** Whether to show percentage label */\n showPercentage?: boolean;\n /** Whether to animate */\n animated?: boolean;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction getProgressColor(value: number, color: ProgressBarProps['color']): string {\n if (color === 'dynamic') {\n if (value >= 75) return 'bg-status-success-bg';\n if (value >= 50) return 'bg-status-warning-bg';\n return 'bg-status-error-bg';\n }\n\n switch (color) {\n case 'success':\n return 'bg-status-success-bg';\n case 'warning':\n return 'bg-status-warning-bg';\n case 'error':\n return 'bg-status-error-bg';\n case 'default':\n default:\n return 'bg-action-primary-bg';\n }\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport const ProgressBar: FC<ProgressBarProps> = memo(function ProgressBar({\n value,\n target,\n milestones = [],\n height = 8,\n color = 'default',\n showPercentage = false,\n animated = true,\n}) {\n const clampedValue = useMemo(() => Math.max(0, Math.min(100, value)), [value]);\n\n const progressColor = getProgressColor(clampedValue, color);\n\n return (\n <div className=\"w-full\">\n {/* Progress bar container */}\n <div className=\"relative w-full rounded-full bg-bg-sunken overflow-visible\" style={{ height }}>\n {/* Progress fill */}\n <div\n className={`\n absolute inset-y-0 left-0 rounded-full\n ${progressColor}\n ${animated ? 'transition-all duration-500 ease-out' : ''}\n `}\n style={{ width: `${clampedValue}%` }}\n role=\"progressbar\"\n aria-valuenow={clampedValue}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n\n {/* Target marker */}\n {target !== undefined && target >= 0 && target <= 100 && (\n <div\n className=\"absolute top-1/2 -translate-y-1/2 w-0.5 h-[150%] bg-text-primary z-10\"\n style={{ left: `${target}%` }}\n title={`Target: ${target}%`}\n >\n {/* Target flag */}\n <div className=\"absolute -top-3 left-1/2 -translate-x-1/2\">\n <svg className=\"w-3 h-3 text-text-primary\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <path d=\"M5 21V3h14l-3 9 3 9H5z\" />\n </svg>\n </div>\n </div>\n )}\n\n {/* Milestones */}\n {milestones.map((milestone, index) => (\n <div\n key={index}\n className={`\n absolute top-1/2 -translate-y-1/2\n w-3 h-3 rounded-full border-2\n ${\n milestone.reached || clampedValue >= milestone.position\n ? 'bg-bg-surface border-action-primary-bg'\n : 'bg-bg-sunken border-border-default'\n }\n z-10\n `}\n style={{ left: `${milestone.position}%`, transform: 'translate(-50%, -50%)' }}\n title={milestone.label || `${milestone.position}%`}\n />\n ))}\n </div>\n\n {/* Milestone labels */}\n {milestones.some((m) => m.label) && (\n <div className=\"relative mt-2\">\n {milestones.map((milestone, index) =>\n milestone.label ? (\n <span\n key={index}\n className=\"absolute text-xs text-text-secondary -translate-x-1/2 whitespace-nowrap\"\n style={{ left: `${milestone.position}%` }}\n >\n {milestone.label}\n </span>\n ) : null\n )}\n </div>\n )}\n\n {/* Percentage label */}\n {showPercentage && (\n <div className=\"flex justify-between items-center mt-1\">\n <span className=\"text-xs text-text-secondary\">0%</span>\n <span className=\"text-sm font-medium text-text-primary\">{clampedValue.toFixed(0)}%</span>\n <span className=\"text-xs text-text-secondary\">100%</span>\n </div>\n )}\n </div>\n );\n});\n\nexport default ProgressBar;\n"],"mappings":";;;AA0CA,SAAS,EAAiB,GAAe,GAA0C;AACjF,KAAI,MAAU,UAGZ,QAFI,KAAS,KAAW,yBACpB,KAAS,KAAW,yBACjB;AAGT,SAAQ,GAAR;EACE,KAAK,UACH,QAAO;EACT,KAAK,UACH,QAAO;EACT,KAAK,QACH,QAAO;EAET,QACE,QAAO;;;AAQb,IAAa,IAAoC,EAAK,SAAqB,EACzE,UACA,WACA,gBAAa,EAAE,EACf,YAAS,GACT,WAAQ,WACR,oBAAiB,IACjB,cAAW,MACV;CACD,IAAM,IAAe,QAAc,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,EAAM,CAAC,EAAE,CAAC,EAAM,CAAC,EAExE,IAAgB,EAAiB,GAAc,EAAM;AAE3D,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;IAA6D,OAAO,EAAE,WAAQ;cAA7F;KAEE,kBAAC,OAAD;MACE,WAAW;;cAEP,EAAc;cACd,IAAW,yCAAyC,GAAG;;MAE3D,OAAO,EAAE,OAAO,GAAG,EAAa,IAAI;MACpC,MAAK;MACL,iBAAe;MACf,iBAAe;MACf,iBAAe;MACf,CAAA;KAGD,MAAW,KAAA,KAAa,KAAU,KAAK,KAAU,OAChD,kBAAC,OAAD;MACE,WAAU;MACV,OAAO,EAAE,MAAM,GAAG,EAAO,IAAI;MAC7B,OAAO,WAAW,EAAO;gBAGzB,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,OAAD;QAAK,WAAU;QAA4B,MAAK;QAAe,SAAQ;kBACrE,kBAAC,QAAD,EAAM,GAAE,0BAA2B,CAAA;QAC/B,CAAA;OACF,CAAA;MACF,CAAA;KAIP,EAAW,KAAK,GAAW,MAC1B,kBAAC,OAAD;MAEE,WAAW;;;gBAIP,EAAU,WAAW,KAAgB,EAAU,WAC3C,2CACA,qCACL;;;MAGH,OAAO;OAAE,MAAM,GAAG,EAAU,SAAS;OAAI,WAAW;OAAyB;MAC7E,OAAO,EAAU,SAAS,GAAG,EAAU,SAAS;MAChD,EAbK,EAaL,CACF;KACE;;GAGL,EAAW,MAAM,MAAM,EAAE,MAAM,IAC9B,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAW,KAAK,GAAW,MAC1B,EAAU,QACR,kBAAC,QAAD;KAEE,WAAU;KACV,OAAO,EAAE,MAAM,GAAG,EAAU,SAAS,IAAI;eAExC,EAAU;KACN,EALA,EAKA,GACL,KACL;IACG,CAAA;GAIP,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD;MAAM,WAAU;gBAA8B;MAAS,CAAA;KACvD,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CAAyD,EAAa,QAAQ,EAAE,EAAC,IAAQ;;KACzF,kBAAC,QAAD;MAAM,WAAU;gBAA8B;MAAW,CAAA;KACrD;;GAEJ;;EAER"}
@@ -132,7 +132,7 @@ var i = e(function({ config: e, onChange: i }) {
132
132
  /* @__PURE__ */ n("button", {
133
133
  type: "button",
134
134
  onClick: () => c(t),
135
- className: "text-state-error hover:text-state-error/80 p-1",
135
+ className: "text-status-error-text hover:text-status-error-text/80 p-1",
136
136
  children: /* @__PURE__ */ n("svg", {
137
137
  className: "w-4 h-4",
138
138
  fill: "none",