@pattern-stack/frontend-patterns 0.0.6 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/atoms/composed/index.d.ts +0 -1
  2. package/dist/atoms/composed/index.d.ts.map +1 -1
  3. package/dist/atoms/types/index.d.ts +0 -2
  4. package/dist/atoms/types/index.d.ts.map +1 -1
  5. package/dist/atoms/ui/ErrorBoundary.d.ts +1 -1
  6. package/dist/atoms/ui/button.d.ts +1 -1
  7. package/dist/atoms/utils/utils.d.ts +0 -2
  8. package/dist/atoms/utils/utils.d.ts.map +1 -1
  9. package/dist/features/auth/components/ProtectedRoute.d.ts +1 -1
  10. package/dist/frontend-patterns.css +1 -1
  11. package/dist/index.es.js +15 -403
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/index.js +14 -403
  14. package/dist/index.js.map +1 -1
  15. package/dist/molecules/layout/Sidebar.d.ts.map +1 -1
  16. package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts +0 -2
  17. package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts.map +1 -1
  18. package/dist/molecules/layout/index.d.ts +0 -3
  19. package/dist/molecules/layout/index.d.ts.map +1 -1
  20. package/dist/templates/factory.d.ts +1 -2
  21. package/dist/templates/factory.d.ts.map +1 -1
  22. package/dist/templates/index.d.ts.map +1 -1
  23. package/package.json +3 -7
  24. package/src/App.tsx +1 -11
  25. package/src/atoms/composed/index.ts +0 -1
  26. package/src/atoms/types/index.ts +1 -3
  27. package/src/atoms/utils/utils.ts +2 -4
  28. package/src/molecules/layout/Sidebar.tsx +23 -10
  29. package/src/molecules/layout/SidebarButton/SidebarButton.tsx +10 -32
  30. package/src/molecules/layout/index.ts +1 -4
  31. package/src/organisms/index.ts +1 -5
  32. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +75 -77
  33. package/src/pages/AdminShowcase/index.tsx +1 -2
  34. package/src/pages/index.ts +1 -2
  35. package/src/templates/factory.tsx +7 -14
  36. package/src/templates/index.ts +0 -4
  37. package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts +0 -19
  38. package/dist/atoms/composed/SalesPanel/SalesPanel.d.ts.map +0 -1
  39. package/dist/atoms/composed/SalesPanel/index.d.ts +0 -2
  40. package/dist/atoms/composed/SalesPanel/index.d.ts.map +0 -1
  41. package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts +0 -63
  42. package/dist/atoms/composed/SalesPanel/mockSalesData.d.ts.map +0 -1
  43. package/dist/atoms/types/entity-config.d.ts +0 -117
  44. package/dist/atoms/types/entity-config.d.ts.map +0 -1
  45. package/dist/atoms/types/navigation.d.ts +0 -30
  46. package/dist/atoms/types/navigation.d.ts.map +0 -1
  47. package/dist/atoms/utils/icon-resolver.d.ts +0 -72
  48. package/dist/atoms/utils/icon-resolver.d.ts.map +0 -1
  49. package/dist/atoms/utils/metric-engine.d.ts +0 -30
  50. package/dist/atoms/utils/metric-engine.d.ts.map +0 -1
  51. package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts +0 -16
  52. package/dist/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.d.ts.map +0 -1
  53. package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts +0 -2
  54. package/dist/molecules/layout/DashboardWithSidePanel/index.d.ts.map +0 -1
  55. package/dist/molecules/layout/NavigationContext.d.ts +0 -15
  56. package/dist/molecules/layout/NavigationContext.d.ts.map +0 -1
  57. package/src/__tests__/atoms/composed/databadge.test.tsx +0 -106
  58. package/src/__tests__/atoms/composed/statcard.test.tsx +0 -133
  59. package/src/__tests__/atoms/utils/icon-resolver.test.tsx +0 -140
  60. package/src/atoms/composed/SalesPanel/SalesPanel.tsx +0 -116
  61. package/src/atoms/composed/SalesPanel/index.ts +0 -1
  62. package/src/atoms/composed/SalesPanel/mockSalesData.ts +0 -151
  63. package/src/atoms/types/entity-config.ts +0 -127
  64. package/src/atoms/types/navigation.ts +0 -43
  65. package/src/atoms/utils/icon-resolver.tsx +0 -54
  66. package/src/atoms/utils/metric-engine.ts +0 -236
  67. package/src/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.tsx +0 -42
  68. package/src/molecules/layout/DashboardWithSidePanel/index.ts +0 -1
  69. package/src/molecules/layout/NavigationContext.tsx +0 -63
  70. package/src/organisms/entity/CategoryBreakdownPanel.tsx +0 -427
  71. package/src/organisms/entity/EntityListPanel.tsx +0 -339
  72. package/src/organisms/entity/MetricsOverviewPanel.tsx +0 -236
  73. package/src/organisms/entity/TrendAnalysisPanel.tsx +0 -337
  74. package/src/organisms/entity/index.ts +0 -4
  75. package/src/pages/AdminShowcase/SalesPerformanceDashboard.tsx +0 -158
  76. package/src/pages/EntityShowcase/EntityManagementShowcase.tsx +0 -137
  77. package/src/pages/EntityShowcase/EntityPerformanceShowcase.tsx +0 -117
  78. package/src/pages/EntityShowcase/index.ts +0 -2
  79. package/src/pages/EntityTemplateExample.tsx +0 -229
  80. package/src/pages/TestEntityTemplate.tsx +0 -40
  81. package/src/templates/entity/EntityManagementTemplate.tsx +0 -430
  82. package/src/templates/entity/EntityPerformanceDashboardTemplate.tsx +0 -277
  83. package/src/templates/entity/configs/financial-config.ts +0 -141
  84. package/src/templates/entity/configs/index.ts +0 -1
  85. package/src/templates/entity/index.ts +0 -3
  86. package/src/templates/financial/FinancialDashboardTemplate.tsx +0 -326
@@ -1,339 +0,0 @@
1
- import React, { useMemo } from 'react';
2
- import { DataTable } from '../../atoms/composed/DataTable';
3
- import type { Column } from '../../atoms/composed/DataTable';
4
- import { Button } from '../../atoms/ui/button';
5
- import { Badge } from '../../atoms/ui/Badge';
6
- import { Card } from '../../atoms/ui/card';
7
- import type { EntityData, EntityTemplateConfig, ActionConfig } from '../../atoms/types';
8
- import { cn } from '../../atoms/utils/utils';
9
- import { Download, Filter, Plus, RefreshCw } from 'lucide-react';
10
-
11
- export interface EntityListPanelProps<T extends EntityData> {
12
- config: EntityTemplateConfig<T>;
13
- data: T[];
14
- isLoading?: boolean;
15
- onRowClick?: (item: T) => void;
16
- onAction?: (action: ActionConfig, selectedItems: T[]) => void;
17
- className?: string;
18
-
19
- // Table configuration
20
- columns?: Column<T>[];
21
- showSearch?: boolean;
22
- showPagination?: boolean;
23
- pageSize?: number;
24
- searchPlaceholder?: string;
25
-
26
- // Business logic features
27
- enableSelection?: boolean;
28
- enableBulkActions?: boolean;
29
- enableExport?: boolean;
30
- enableFiltering?: boolean;
31
- enableRefresh?: boolean;
32
-
33
- // Extension points
34
- renderToolbar?: () => React.ReactNode;
35
- renderBulkActions?: (selectedItems: T[]) => React.ReactNode;
36
- renderCustomColumn?: (column: Column<T>, item: T) => React.ReactNode;
37
- renderEmptyState?: () => React.ReactNode;
38
- headerSlot?: React.ReactNode;
39
- footerSlot?: React.ReactNode;
40
-
41
- // Event handlers
42
- onExport?: (data: T[]) => void;
43
- onRefresh?: () => void;
44
- onFilter?: (filters: Record<string, unknown>) => void;
45
- }
46
-
47
- export const EntityListPanel = <T extends EntityData>({
48
- config,
49
- data,
50
- isLoading = false,
51
- onRowClick,
52
- onAction,
53
- className,
54
- columns: customColumns,
55
- showSearch = true,
56
- showPagination = true,
57
- pageSize = 10,
58
- searchPlaceholder,
59
- enableSelection = false,
60
- enableBulkActions = false,
61
- enableExport = true,
62
- enableFiltering = false,
63
- enableRefresh = true,
64
- renderToolbar,
65
- renderBulkActions,
66
- renderCustomColumn,
67
- renderEmptyState,
68
- headerSlot,
69
- footerSlot,
70
- onExport,
71
- onRefresh
72
- }: EntityListPanelProps<T>) => {
73
- const [selectedItems, setSelectedItems] = React.useState<T[]>([]);
74
- const [showFilters, setShowFilters] = React.useState(false);
75
-
76
- // Generate columns based on config if not provided
77
- const tableColumns = useMemo(() => {
78
- if (customColumns) return customColumns;
79
-
80
- const generatedColumns: Column<T>[] = [];
81
-
82
- // Add selection column if enabled
83
- if (enableSelection) {
84
- generatedColumns.push({
85
- key: '__selection',
86
- header: (
87
- <input
88
- type="checkbox"
89
- checked={selectedItems.length === data.length && data.length > 0}
90
- onChange={(e) => {
91
- if (e.target.checked) {
92
- setSelectedItems([...data]);
93
- } else {
94
- setSelectedItems([]);
95
- }
96
- }}
97
- />
98
- ),
99
- cell: (item: T) => (
100
- <input
101
- type="checkbox"
102
- checked={selectedItems.some(selected => selected.id === item.id)}
103
- onChange={(e) => {
104
- if (e.target.checked) {
105
- setSelectedItems(prev => [...prev, item]);
106
- } else {
107
- setSelectedItems(prev => prev.filter(selected => selected.id !== item.id));
108
- }
109
- }}
110
- />
111
- ),
112
- sortable: false,
113
- width: '50px'
114
- });
115
- }
116
-
117
- // Auto-generate columns from first data item
118
- if (data.length > 0) {
119
- const sampleItem = data[0];
120
- Object.keys(sampleItem).forEach(key => {
121
- if (key === 'id') return; // Skip ID column usually
122
-
123
- const isStatus = key.toLowerCase().includes('status');
124
- const isCategory = key.toLowerCase().includes('category') || key.toLowerCase().includes('type');
125
- const isDate = key.toLowerCase().includes('date') || key.toLowerCase().includes('time');
126
- const isAmount = key.toLowerCase().includes('amount') || key.toLowerCase().includes('price') || key.toLowerCase().includes('cost');
127
-
128
- generatedColumns.push({
129
- key,
130
- header: key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1'),
131
- type: isStatus ? 'status' : isCategory ? 'category' : 'default',
132
- sortable: true,
133
- cell: renderCustomColumn ? (item: T) => {
134
- const column = { key, header: '', type: isStatus ? 'status' : isCategory ? 'category' : 'default' } as Column<T>;
135
- const customContent = renderCustomColumn(column, item);
136
- if (customContent) return customContent;
137
-
138
- // Default rendering with smart formatting
139
- const value = item[key];
140
- if (isDate && value) {
141
- return new Date(value as string).toLocaleDateString();
142
- }
143
- if (isAmount && typeof value === 'number') {
144
- return new Intl.NumberFormat('en-US', {
145
- style: 'currency',
146
- currency: 'USD'
147
- }).format(value);
148
- }
149
- return value?.toString() || '-';
150
- } : undefined
151
- });
152
- });
153
- }
154
-
155
- return generatedColumns;
156
- }, [customColumns, data, enableSelection, selectedItems, renderCustomColumn]);
157
-
158
- const handleExport = () => {
159
- if (onExport) {
160
- onExport(selectedItems.length > 0 ? selectedItems : data);
161
- } else {
162
- // Default CSV export
163
- const headers = tableColumns.filter(col => col.key !== '__selection').map(col => col.key);
164
- const csvContent = [
165
- headers.join(','),
166
- ...(selectedItems.length > 0 ? selectedItems : data).map(item =>
167
- headers.map(header => item[header] || '').join(',')
168
- )
169
- ].join('\n');
170
-
171
- const blob = new Blob([csvContent], { type: 'text/csv' });
172
- const url = URL.createObjectURL(blob);
173
- const a = document.createElement('a');
174
- a.href = url;
175
- a.download = `${config.display.title.toLowerCase().replace(/\s+/g, '-')}-export.csv`;
176
- a.click();
177
- URL.revokeObjectURL(url);
178
- }
179
- };
180
-
181
- const renderToolbarSection = () => {
182
- return (
183
- <div className="flex items-center justify-between gap-4 mb-4">
184
- <div className="flex items-center gap-2">
185
- <h3 className="text-lg font-semibold text-foreground">
186
- {config.display.title}
187
- </h3>
188
- {config.display.description && (
189
- <Badge variant="outline" className="text-xs">
190
- {data.length} items
191
- </Badge>
192
- )}
193
- </div>
194
-
195
- <div className="flex items-center gap-2">
196
- {renderToolbar && renderToolbar()}
197
-
198
- {enableFiltering && (
199
- <Button
200
- variant="outline"
201
- size="sm"
202
- onClick={() => setShowFilters(!showFilters)}
203
- className={cn(showFilters && "bg-muted")}
204
- >
205
- <Filter className="w-4 h-4 mr-2" />
206
- Filters
207
- </Button>
208
- )}
209
-
210
- {enableRefresh && (
211
- <Button
212
- variant="outline"
213
- size="sm"
214
- onClick={onRefresh}
215
- disabled={isLoading}
216
- >
217
- <RefreshCw className={cn("w-4 h-4 mr-2", isLoading && "animate-spin")} />
218
- Refresh
219
- </Button>
220
- )}
221
-
222
- {enableExport && (
223
- <Button
224
- variant="outline"
225
- size="sm"
226
- onClick={handleExport}
227
- disabled={data.length === 0}
228
- >
229
- <Download className="w-4 h-4 mr-2" />
230
- Export
231
- </Button>
232
- )}
233
-
234
- {config.actions?.filter(action => action.type === 'primary').map(action => (
235
- <Button
236
- key={action.label}
237
- size="sm"
238
- onClick={() => onAction?.(action, [])}
239
- disabled={isLoading}
240
- >
241
- {action.icon && <action.icon className="w-4 h-4 mr-2" />}
242
- {action.label}
243
- </Button>
244
- ))}
245
- </div>
246
- </div>
247
- );
248
- };
249
-
250
- const renderBulkActionsSection = () => {
251
- if (!enableBulkActions || selectedItems.length === 0) return null;
252
-
253
- return (
254
- <div className="flex items-center justify-between bg-muted/50 p-3 rounded-md mb-4">
255
- <div className="flex items-center gap-2">
256
- <span className="text-sm text-muted-foreground">
257
- {selectedItems.length} items selected
258
- </span>
259
- <Button
260
- variant="ghost"
261
- size="sm"
262
- onClick={() => setSelectedItems([])}
263
- >
264
- Clear selection
265
- </Button>
266
- </div>
267
-
268
- <div className="flex items-center gap-2">
269
- {renderBulkActions && renderBulkActions(selectedItems)}
270
-
271
- {config.actions?.filter(action => action.type !== 'primary').map(action => (
272
- <Button
273
- key={action.label}
274
- variant={action.type === 'danger' ? 'destructive' : 'outline'}
275
- size="sm"
276
- onClick={() => onAction?.(action, selectedItems)}
277
- >
278
- {action.icon && <action.icon className="w-4 h-4 mr-2" />}
279
- {action.label}
280
- </Button>
281
- ))}
282
- </div>
283
- </div>
284
- );
285
- };
286
-
287
- const renderEmptyStateSection = () => {
288
- if (data.length > 0) return null;
289
-
290
- if (renderEmptyState) {
291
- return renderEmptyState();
292
- }
293
-
294
- return (
295
- <div className="text-center py-12">
296
- <div className="text-muted-foreground mb-4">
297
- No {config.display.title.toLowerCase()} found
298
- </div>
299
- {config.actions?.find(action => action.type === 'primary') && (
300
- <Button onClick={() => {
301
- const primaryAction = config.actions?.find(action => action.type === 'primary');
302
- if (primaryAction) onAction?.(primaryAction, []);
303
- }}>
304
- <Plus className="w-4 h-4 mr-2" />
305
- Add {config.display.title.slice(0, -1)}
306
- </Button>
307
- )}
308
- </div>
309
- );
310
- };
311
-
312
- return (
313
- <Card className={cn('p-6', className)} category={config.display.category} data-component-name="EntityListPanel">
314
- {headerSlot}
315
-
316
- {renderToolbarSection()}
317
- {renderBulkActionsSection()}
318
-
319
- {data.length === 0 ? (
320
- renderEmptyStateSection()
321
- ) : (
322
- <DataTable
323
- data={data}
324
- columns={tableColumns}
325
- isLoading={isLoading}
326
- showSearch={showSearch}
327
- showPagination={showPagination}
328
- pageSize={pageSize}
329
- searchPlaceholder={searchPlaceholder || `Search ${config.display.title.toLowerCase()}...`}
330
- onRowClick={onRowClick}
331
- hover={!!onRowClick}
332
- emptyMessage={`No ${config.display.title.toLowerCase()} found`}
333
- />
334
- )}
335
-
336
- {footerSlot}
337
- </Card>
338
- );
339
- };
@@ -1,236 +0,0 @@
1
- import React from 'react';
2
- import { StatCard } from '../../atoms/composed/StatCard';
3
- import { MetricCalculationEngine } from '../../atoms/utils/metric-engine';
4
- import type { MetricConfig, EntityData, MetricValue } from '../../atoms/types';
5
- import { cn } from '../../atoms/utils/utils';
6
-
7
- export interface MetricsOverviewPanelProps {
8
- metrics: MetricConfig[];
9
- data: EntityData[];
10
- previousData?: EntityData[];
11
- isLoading?: boolean;
12
- onMetricClick?: (metric: MetricConfig, value: MetricValue) => void;
13
- className?: string;
14
- layout?: 'grid' | 'horizontal' | 'vertical';
15
- columns?: 2 | 3 | 4;
16
- category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
17
-
18
- // Extension points
19
- renderCustomMetric?: (metric: MetricConfig, value: MetricValue, index: number) => React.ReactNode;
20
- renderHeader?: () => React.ReactNode;
21
- renderFooter?: () => React.ReactNode;
22
- headerSlot?: React.ReactNode;
23
- footerSlot?: React.ReactNode;
24
- }
25
-
26
- export const MetricsOverviewPanel: React.FC<MetricsOverviewPanelProps> = ({
27
- metrics,
28
- data,
29
- previousData,
30
- isLoading = false,
31
- onMetricClick,
32
- className,
33
- layout = 'grid',
34
- columns = 4,
35
- category,
36
- renderCustomMetric,
37
- renderHeader,
38
- renderFooter,
39
- headerSlot,
40
- footerSlot
41
- }) => {
42
- const calculatedMetrics = React.useMemo(() => {
43
- if (isLoading || !data?.length || !metrics?.length) return {};
44
-
45
- return metrics.reduce((acc, metric) => {
46
- acc[metric.key] = MetricCalculationEngine.calculateMetric(
47
- metric,
48
- data,
49
- previousData
50
- );
51
- return acc;
52
- }, {} as Record<string, MetricValue>);
53
- }, [metrics, data, previousData, isLoading]);
54
-
55
- const getLayoutClasses = () => {
56
- switch (layout) {
57
- case 'horizontal':
58
- return 'flex flex-wrap gap-4';
59
- case 'vertical':
60
- return 'flex flex-col gap-4';
61
- case 'grid':
62
- default:
63
- return cn(
64
- 'grid gap-4',
65
- {
66
- 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4': columns === 4,
67
- 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3': columns === 3,
68
- 'grid-cols-1 sm:grid-cols-2': columns === 2
69
- }
70
- );
71
- }
72
- };
73
-
74
- const renderMetricCard = (metric: MetricConfig, index: number) => {
75
- if (renderCustomMetric) {
76
- const customContent = renderCustomMetric(metric, calculatedMetrics[metric.key], index);
77
- if (customContent) return customContent;
78
- }
79
-
80
- const metricValue = calculatedMetrics[metric.key];
81
-
82
- if (isLoading || !metricValue) {
83
- return (
84
- <StatCard
85
- key={metric.key}
86
- title={metric.label}
87
- value=""
88
- isLoading={true}
89
- category={category}
90
- icon={metric.icon ? <metric.icon /> : undefined}
91
- />
92
- );
93
- }
94
-
95
- const trend = metricValue.trend !== 'neutral' && metricValue.previous
96
- ? {
97
- value: Number(((metricValue.current - metricValue.previous) / Math.abs(metricValue.previous) * 100).toFixed(1)),
98
- label: 'vs last period'
99
- }
100
- : undefined;
101
-
102
- const formatTargetValue = (value: number) => {
103
- switch (metric.type) {
104
- case 'currency':
105
- return new Intl.NumberFormat('en-US', {
106
- style: 'currency',
107
- currency: 'USD'
108
- }).format(value);
109
- case 'percentage':
110
- return `${value}%`;
111
- default:
112
- return value.toString();
113
- }
114
- };
115
-
116
- const subtitle = metricValue.target
117
- ? `Target: ${formatTargetValue(metricValue.target)}`
118
- : undefined;
119
-
120
- return (
121
- <StatCard
122
- key={metric.key}
123
- title={metric.label}
124
- value={metricValue.formattedValue}
125
- subtitle={subtitle}
126
- trend={trend}
127
- category={category}
128
- icon={metric.icon ? <metric.icon /> : undefined}
129
- onClick={onMetricClick ? () => onMetricClick(metric, metricValue) : undefined}
130
- valueTooltip={`Current: ${metricValue.formattedValue}${metricValue.previous ? ` | Previous: ${formatTargetValue(metricValue.previous)}` : ''}`}
131
- iconTooltip={`View ${metric.label} details`}
132
- />
133
- );
134
- };
135
-
136
- return (
137
- <div className={cn('space-y-4', className)} data-component-name="MetricsOverviewPanel">
138
- {/* Header Extension Point */}
139
- {headerSlot}
140
- {renderHeader && renderHeader()}
141
-
142
- {/* Metrics Grid */}
143
- <div className={getLayoutClasses()}>
144
- {metrics.map((metric, index) => renderMetricCard(metric, index))}
145
- </div>
146
-
147
- {/* Footer Extension Point */}
148
- {renderFooter && renderFooter()}
149
- {footerSlot}
150
- </div>
151
- );
152
- };
153
-
154
- // Enhanced version with insights
155
- export interface MetricsOverviewWithInsightsPanelProps extends MetricsOverviewPanelProps {
156
- showInsights?: boolean;
157
- insightThresholds?: Record<string, { warning: number; critical: number }>;
158
- renderInsight?: (insight: { type: 'positive' | 'negative' | 'neutral'; message: string; value?: number }, index: number) => React.ReactNode;
159
- }
160
-
161
- export const MetricsOverviewWithInsightsPanel: React.FC<MetricsOverviewWithInsightsPanelProps> = ({
162
- showInsights = true,
163
- insightThresholds,
164
- renderInsight,
165
- ...props
166
- }) => {
167
- // Ensure metrics is always an array
168
- const safeMetrics = props.metrics || [];
169
- const safeData = props.data || [];
170
-
171
- const calculatedMetrics = React.useMemo(() => {
172
- if (props.isLoading || !safeData.length || !safeMetrics.length) return {};
173
-
174
- return safeMetrics.reduce((acc, metric) => {
175
- acc[metric.key] = MetricCalculationEngine.calculateMetric(
176
- metric,
177
- safeData,
178
- props.previousData
179
- );
180
- return acc;
181
- }, {} as Record<string, MetricValue>);
182
- }, [safeMetrics, safeData, props.previousData, props.isLoading]);
183
-
184
- const insights = React.useMemo(() => {
185
- if (!showInsights || props.isLoading || Object.keys(calculatedMetrics).length === 0) {
186
- return [];
187
- }
188
-
189
- return MetricCalculationEngine.detectInsights(calculatedMetrics, insightThresholds);
190
- }, [calculatedMetrics, showInsights, insightThresholds, props.isLoading]);
191
-
192
- const renderInsightsSection = () => {
193
- if (!showInsights || insights.length === 0) return null;
194
-
195
- return (
196
- <div className="space-y-2">
197
- <h3 className="text-sm font-semibold text-foreground">Key Insights</h3>
198
- <div className="space-y-1">
199
- {insights.map((insight, index) => {
200
- if (renderInsight) {
201
- const customInsight = renderInsight(insight, index);
202
- if (customInsight) return customInsight;
203
- }
204
-
205
- const colorClass = {
206
- positive: 'text-status-success',
207
- negative: 'text-status-error',
208
- neutral: 'text-muted-foreground'
209
- }[insight.type];
210
-
211
- return (
212
- <div
213
- key={index}
214
- className={cn('text-sm', colorClass)}
215
- >
216
- • {insight.message}
217
- </div>
218
- );
219
- })}
220
- </div>
221
- </div>
222
- );
223
- };
224
-
225
- return (
226
- <MetricsOverviewPanel
227
- {...props}
228
- footerSlot={
229
- <>
230
- {renderInsightsSection()}
231
- {props.footerSlot}
232
- </>
233
- }
234
- />
235
- );
236
- };