@pattern-stack/frontend-patterns 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/dist/frontend-patterns.css +1 -1
  2. package/dist/index.es.js +1917 -2
  3. package/dist/index.es.js.map +1 -1
  4. package/dist/index.js +1916 -1
  5. package/dist/index.js.map +1 -1
  6. package/package.json +6 -3
  7. package/src/App.tsx +11 -1
  8. package/src/atoms/composed/SalesPanel/SalesPanel.tsx +116 -0
  9. package/src/atoms/composed/SalesPanel/index.ts +1 -0
  10. package/src/atoms/composed/SalesPanel/mockSalesData.ts +151 -0
  11. package/src/atoms/composed/index.ts +1 -0
  12. package/src/atoms/types/entity-config.ts +127 -0
  13. package/src/atoms/types/index.ts +2 -1
  14. package/src/atoms/utils/metric-engine.ts +236 -0
  15. package/src/atoms/utils/utils.ts +2 -1
  16. package/src/molecules/layout/DashboardWithSidePanel/DashboardWithSidePanel.tsx +42 -0
  17. package/src/molecules/layout/DashboardWithSidePanel/index.ts +1 -0
  18. package/src/molecules/layout/Sidebar.tsx +10 -2
  19. package/src/molecules/layout/index.ts +1 -0
  20. package/src/organisms/entity/CategoryBreakdownPanel.tsx +427 -0
  21. package/src/organisms/entity/EntityListPanel.tsx +339 -0
  22. package/src/organisms/entity/MetricsOverviewPanel.tsx +236 -0
  23. package/src/organisms/entity/TrendAnalysisPanel.tsx +337 -0
  24. package/src/organisms/entity/index.ts +4 -0
  25. package/src/organisms/index.ts +4 -1
  26. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +77 -75
  27. package/src/pages/AdminShowcase/SalesPerformanceDashboard.tsx +158 -0
  28. package/src/pages/AdminShowcase/index.tsx +2 -1
  29. package/src/pages/EntityShowcase/EntityManagementShowcase.tsx +137 -0
  30. package/src/pages/EntityShowcase/EntityPerformanceShowcase.tsx +117 -0
  31. package/src/pages/EntityShowcase/index.ts +2 -0
  32. package/src/pages/EntityTemplateExample.tsx +229 -0
  33. package/src/pages/TestEntityTemplate.tsx +40 -0
  34. package/src/pages/index.ts +2 -1
  35. package/src/templates/entity/EntityManagementTemplate.tsx +430 -0
  36. package/src/templates/entity/EntityPerformanceDashboardTemplate.tsx +277 -0
  37. package/src/templates/entity/configs/financial-config.ts +141 -0
  38. package/src/templates/entity/configs/index.ts +1 -0
  39. package/src/templates/entity/index.ts +3 -0
  40. package/src/templates/financial/FinancialDashboardTemplate.tsx +326 -0
  41. package/src/templates/index.ts +3 -0
  42. package/dist/atoms/composed/Accordion/Accordion.d.ts +0 -20
  43. package/dist/atoms/composed/Accordion/Accordion.d.ts.map +0 -1
  44. package/dist/atoms/composed/Accordion/index.d.ts +0 -2
  45. package/dist/atoms/composed/Accordion/index.d.ts.map +0 -1
  46. package/dist/atoms/composed/Alert/Alert.d.ts +0 -25
  47. package/dist/atoms/composed/Alert/Alert.d.ts.map +0 -1
  48. package/dist/atoms/composed/Alert/index.d.ts +0 -2
  49. package/dist/atoms/composed/Alert/index.d.ts.map +0 -1
  50. package/dist/atoms/composed/Breadcrumb/Breadcrumb.d.ts +0 -17
  51. package/dist/atoms/composed/Breadcrumb/Breadcrumb.d.ts.map +0 -1
  52. package/dist/atoms/composed/Breadcrumb/index.d.ts +0 -2
  53. package/dist/atoms/composed/Breadcrumb/index.d.ts.map +0 -1
  54. package/dist/atoms/composed/Chart/Chart.d.ts +0 -37
  55. package/dist/atoms/composed/Chart/Chart.d.ts.map +0 -1
  56. package/dist/atoms/composed/Chart/index.d.ts +0 -3
  57. package/dist/atoms/composed/Chart/index.d.ts.map +0 -1
  58. package/dist/atoms/composed/ColorSwatch/ColorSwatch.d.ts +0 -19
  59. package/dist/atoms/composed/ColorSwatch/ColorSwatch.d.ts.map +0 -1
  60. package/dist/atoms/composed/ColorSwatch/index.d.ts +0 -2
  61. package/dist/atoms/composed/ColorSwatch/index.d.ts.map +0 -1
  62. package/dist/atoms/composed/DarkModeToggle.d.ts +0 -4
  63. package/dist/atoms/composed/DarkModeToggle.d.ts.map +0 -1
  64. package/dist/atoms/composed/DataBadge/DataBadge.d.ts +0 -13
  65. package/dist/atoms/composed/DataBadge/DataBadge.d.ts.map +0 -1
  66. package/dist/atoms/composed/DataBadge/index.d.ts +0 -2
  67. package/dist/atoms/composed/DataBadge/index.d.ts.map +0 -1
  68. package/dist/atoms/composed/DataTable/DataTable.d.ts +0 -28
  69. package/dist/atoms/composed/DataTable/DataTable.d.ts.map +0 -1
  70. package/dist/atoms/composed/DataTable/TableCellWithTooltip.d.ts +0 -10
  71. package/dist/atoms/composed/DataTable/TableCellWithTooltip.d.ts.map +0 -1
  72. package/dist/atoms/composed/DataTable/index.d.ts +0 -3
  73. package/dist/atoms/composed/DataTable/index.d.ts.map +0 -1
  74. package/dist/atoms/composed/DateTimePicker/DateTimePicker.d.ts +0 -45
  75. package/dist/atoms/composed/DateTimePicker/DateTimePicker.d.ts.map +0 -1
  76. package/dist/atoms/composed/DateTimePicker/index.d.ts +0 -3
  77. package/dist/atoms/composed/DateTimePicker/index.d.ts.map +0 -1
  78. package/dist/atoms/composed/DetailedCard/DetailedCard.d.ts +0 -30
  79. package/dist/atoms/composed/DetailedCard/DetailedCard.d.ts.map +0 -1
  80. package/dist/atoms/composed/DetailedCard/index.d.ts +0 -3
  81. package/dist/atoms/composed/DetailedCard/index.d.ts.map +0 -1
  82. package/dist/atoms/composed/EmptyState/EmptyState.d.ts +0 -18
  83. package/dist/atoms/composed/EmptyState/EmptyState.d.ts.map +0 -1
  84. package/dist/atoms/composed/EmptyState/index.d.ts +0 -2
  85. package/dist/atoms/composed/EmptyState/index.d.ts.map +0 -1
  86. package/dist/atoms/composed/FileUpload/FileUpload.d.ts +0 -46
  87. package/dist/atoms/composed/FileUpload/FileUpload.d.ts.map +0 -1
  88. package/dist/atoms/composed/FileUpload/index.d.ts +0 -3
  89. package/dist/atoms/composed/FileUpload/index.d.ts.map +0 -1
  90. package/dist/atoms/composed/FormField/FormField.d.ts +0 -23
  91. package/dist/atoms/composed/FormField/FormField.d.ts.map +0 -1
  92. package/dist/atoms/composed/FormField/index.d.ts +0 -2
  93. package/dist/atoms/composed/FormField/index.d.ts.map +0 -1
  94. package/dist/atoms/composed/GlobalSearch/GlobalSearch.d.ts +0 -8
  95. package/dist/atoms/composed/GlobalSearch/GlobalSearch.d.ts.map +0 -1
  96. package/dist/atoms/composed/GlobalSearch/index.d.ts +0 -2
  97. package/dist/atoms/composed/GlobalSearch/index.d.ts.map +0 -1
  98. package/dist/atoms/composed/IconBadge/IconBadge.d.ts +0 -16
  99. package/dist/atoms/composed/IconBadge/IconBadge.d.ts.map +0 -1
  100. package/dist/atoms/composed/IconBadge/index.d.ts +0 -3
  101. package/dist/atoms/composed/IconBadge/index.d.ts.map +0 -1
  102. package/dist/atoms/composed/Modal/Modal.d.ts +0 -18
  103. package/dist/atoms/composed/Modal/Modal.d.ts.map +0 -1
  104. package/dist/atoms/composed/Modal/index.d.ts +0 -3
  105. package/dist/atoms/composed/Modal/index.d.ts.map +0 -1
  106. package/dist/atoms/composed/PaletteSwitcher.d.ts +0 -7
  107. package/dist/atoms/composed/PaletteSwitcher.d.ts.map +0 -1
  108. package/dist/atoms/composed/ProgressBar/ProgressBar.d.ts +0 -25
  109. package/dist/atoms/composed/ProgressBar/ProgressBar.d.ts.map +0 -1
  110. package/dist/atoms/composed/ProgressBar/index.d.ts +0 -2
  111. package/dist/atoms/composed/ProgressBar/index.d.ts.map +0 -1
  112. package/dist/atoms/composed/StatCard/StatCard.d.ts +0 -21
  113. package/dist/atoms/composed/StatCard/StatCard.d.ts.map +0 -1
  114. package/dist/atoms/composed/StatCard/index.d.ts +0 -2
  115. package/dist/atoms/composed/StatCard/index.d.ts.map +0 -1
  116. package/dist/atoms/composed/StyleGuide.d.ts +0 -3
  117. package/dist/atoms/composed/StyleGuide.d.ts.map +0 -1
  118. package/dist/atoms/composed/Toast/Toast.d.ts +0 -40
  119. package/dist/atoms/composed/Toast/Toast.d.ts.map +0 -1
  120. package/dist/atoms/composed/Toast/index.d.ts +0 -2
  121. package/dist/atoms/composed/Toast/index.d.ts.map +0 -1
  122. package/dist/atoms/composed/Tooltip/Tooltip.d.ts +0 -16
  123. package/dist/atoms/composed/Tooltip/Tooltip.d.ts.map +0 -1
  124. package/dist/atoms/composed/Tooltip/index.d.ts +0 -2
  125. package/dist/atoms/composed/Tooltip/index.d.ts.map +0 -1
  126. package/dist/atoms/composed/UserAvatar/UserAvatar.d.ts +0 -8
  127. package/dist/atoms/composed/UserAvatar/UserAvatar.d.ts.map +0 -1
  128. package/dist/atoms/composed/UserAvatar/index.d.ts +0 -2
  129. package/dist/atoms/composed/UserAvatar/index.d.ts.map +0 -1
  130. package/dist/atoms/composed/UserMenu/UserMenu.d.ts +0 -8
  131. package/dist/atoms/composed/UserMenu/UserMenu.d.ts.map +0 -1
  132. package/dist/atoms/composed/UserMenu/index.d.ts +0 -2
  133. package/dist/atoms/composed/UserMenu/index.d.ts.map +0 -1
  134. package/dist/atoms/composed/index.d.ts +0 -25
  135. package/dist/atoms/composed/index.d.ts.map +0 -1
  136. package/dist/atoms/hooks/useApi.d.ts +0 -25
  137. package/dist/atoms/hooks/useApi.d.ts.map +0 -1
  138. package/dist/atoms/hooks/useHealth.d.ts +0 -19
  139. package/dist/atoms/hooks/useHealth.d.ts.map +0 -1
  140. package/dist/atoms/index.d.ts +0 -9
  141. package/dist/atoms/index.d.ts.map +0 -1
  142. package/dist/atoms/services/api/client.d.ts +0 -20
  143. package/dist/atoms/services/api/client.d.ts.map +0 -1
  144. package/dist/atoms/services/auth-service.d.ts +0 -24
  145. package/dist/atoms/services/auth-service.d.ts.map +0 -1
  146. package/dist/atoms/services/health.d.ts +0 -7
  147. package/dist/atoms/services/health.d.ts.map +0 -1
  148. package/dist/atoms/services/index.d.ts +0 -4
  149. package/dist/atoms/services/index.d.ts.map +0 -1
  150. package/dist/atoms/shared/config/constants.d.ts +0 -15
  151. package/dist/atoms/shared/config/constants.d.ts.map +0 -1
  152. package/dist/atoms/shared/config/dashboard-sizes.d.ts +0 -83
  153. package/dist/atoms/shared/config/dashboard-sizes.d.ts.map +0 -1
  154. package/dist/atoms/shared/config/environment.d.ts +0 -10
  155. package/dist/atoms/shared/config/environment.d.ts.map +0 -1
  156. package/dist/atoms/shared/index.d.ts +0 -4
  157. package/dist/atoms/shared/index.d.ts.map +0 -1
  158. package/dist/atoms/types/auth.d.ts +0 -56
  159. package/dist/atoms/types/auth.d.ts.map +0 -1
  160. package/dist/atoms/types/generated.d.ts +0 -1469
  161. package/dist/atoms/types/generated.d.ts.map +0 -1
  162. package/dist/atoms/types/index.d.ts +0 -4
  163. package/dist/atoms/types/index.d.ts.map +0 -1
  164. package/dist/atoms/types/loading.d.ts +0 -26
  165. package/dist/atoms/types/loading.d.ts.map +0 -1
  166. package/dist/atoms/ui/Badge.d.ts +0 -10
  167. package/dist/atoms/ui/Badge.d.ts.map +0 -1
  168. package/dist/atoms/ui/ErrorBoundary.d.ts +0 -18
  169. package/dist/atoms/ui/ErrorBoundary.d.ts.map +0 -1
  170. package/dist/atoms/ui/Select.d.ts +0 -28
  171. package/dist/atoms/ui/Select.d.ts.map +0 -1
  172. package/dist/atoms/ui/Switch.d.ts +0 -9
  173. package/dist/atoms/ui/Switch.d.ts.map +0 -1
  174. package/dist/atoms/ui/Tabs.d.ts +0 -30
  175. package/dist/atoms/ui/Tabs.d.ts.map +0 -1
  176. package/dist/atoms/ui/avatar.d.ts +0 -7
  177. package/dist/atoms/ui/avatar.d.ts.map +0 -1
  178. package/dist/atoms/ui/button.d.ts +0 -14
  179. package/dist/atoms/ui/button.d.ts.map +0 -1
  180. package/dist/atoms/ui/card.d.ts +0 -12
  181. package/dist/atoms/ui/card.d.ts.map +0 -1
  182. package/dist/atoms/ui/dropdown-menu.d.ts +0 -28
  183. package/dist/atoms/ui/dropdown-menu.d.ts.map +0 -1
  184. package/dist/atoms/ui/index.d.ts +0 -15
  185. package/dist/atoms/ui/index.d.ts.map +0 -1
  186. package/dist/atoms/ui/input.d.ts +0 -5
  187. package/dist/atoms/ui/input.d.ts.map +0 -1
  188. package/dist/atoms/ui/label.d.ts +0 -6
  189. package/dist/atoms/ui/label.d.ts.map +0 -1
  190. package/dist/atoms/ui/skeleton.d.ts +0 -3
  191. package/dist/atoms/ui/skeleton.d.ts.map +0 -1
  192. package/dist/atoms/ui/spinner.d.ts +0 -14
  193. package/dist/atoms/ui/spinner.d.ts.map +0 -1
  194. package/dist/atoms/ui/table.d.ts +0 -11
  195. package/dist/atoms/ui/table.d.ts.map +0 -1
  196. package/dist/atoms/utils/animations.d.ts +0 -65
  197. package/dist/atoms/utils/animations.d.ts.map +0 -1
  198. package/dist/atoms/utils/tooltip-helpers.d.ts +0 -71
  199. package/dist/atoms/utils/tooltip-helpers.d.ts.map +0 -1
  200. package/dist/atoms/utils/utils.d.ts +0 -4
  201. package/dist/atoms/utils/utils.d.ts.map +0 -1
  202. package/dist/features/auth/components/LoginForm.d.ts +0 -2
  203. package/dist/features/auth/components/LoginForm.d.ts.map +0 -1
  204. package/dist/features/auth/components/LogoutButton.d.ts +0 -2
  205. package/dist/features/auth/components/LogoutButton.d.ts.map +0 -1
  206. package/dist/features/auth/components/ProtectedRoute.d.ts +0 -10
  207. package/dist/features/auth/components/ProtectedRoute.d.ts.map +0 -1
  208. package/dist/features/auth/components/index.d.ts +0 -4
  209. package/dist/features/auth/components/index.d.ts.map +0 -1
  210. package/dist/features/auth/hooks/index.d.ts +0 -3
  211. package/dist/features/auth/hooks/index.d.ts.map +0 -1
  212. package/dist/features/auth/hooks/useAuth.d.ts +0 -10
  213. package/dist/features/auth/hooks/useAuth.d.ts.map +0 -1
  214. package/dist/features/auth/hooks/usePermissions.d.ts +0 -13
  215. package/dist/features/auth/hooks/usePermissions.d.ts.map +0 -1
  216. package/dist/features/auth/index.d.ts +0 -3
  217. package/dist/features/auth/index.d.ts.map +0 -1
  218. package/dist/features/index.d.ts +0 -2
  219. package/dist/features/index.d.ts.map +0 -1
  220. package/dist/index.d.ts +0 -10
  221. package/dist/index.d.ts.map +0 -1
  222. package/dist/molecules/forms/FormGroup.d.ts +0 -17
  223. package/dist/molecules/forms/FormGroup.d.ts.map +0 -1
  224. package/dist/molecules/forms/SearchInput.d.ts +0 -36
  225. package/dist/molecules/forms/SearchInput.d.ts.map +0 -1
  226. package/dist/molecules/forms/index.d.ts +0 -3
  227. package/dist/molecules/forms/index.d.ts.map +0 -1
  228. package/dist/molecules/index.d.ts +0 -4
  229. package/dist/molecules/index.d.ts.map +0 -1
  230. package/dist/molecules/layout/AppHeader/AppHeader.d.ts +0 -7
  231. package/dist/molecules/layout/AppHeader/AppHeader.d.ts.map +0 -1
  232. package/dist/molecules/layout/AppHeader/index.d.ts +0 -2
  233. package/dist/molecules/layout/AppHeader/index.d.ts.map +0 -1
  234. package/dist/molecules/layout/AppLayout.d.ts +0 -2
  235. package/dist/molecules/layout/AppLayout.d.ts.map +0 -1
  236. package/dist/molecules/layout/PageTemplate.d.ts +0 -19
  237. package/dist/molecules/layout/PageTemplate.d.ts.map +0 -1
  238. package/dist/molecules/layout/SectionHeader/SectionHeader.d.ts +0 -24
  239. package/dist/molecules/layout/SectionHeader/SectionHeader.d.ts.map +0 -1
  240. package/dist/molecules/layout/SectionHeader/index.d.ts +0 -2
  241. package/dist/molecules/layout/SectionHeader/index.d.ts.map +0 -1
  242. package/dist/molecules/layout/ShowcaseSection.d.ts +0 -22
  243. package/dist/molecules/layout/ShowcaseSection.d.ts.map +0 -1
  244. package/dist/molecules/layout/Sidebar.d.ts +0 -6
  245. package/dist/molecules/layout/Sidebar.d.ts.map +0 -1
  246. package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts +0 -13
  247. package/dist/molecules/layout/SidebarButton/SidebarButton.d.ts.map +0 -1
  248. package/dist/molecules/layout/SidebarButton/index.d.ts +0 -2
  249. package/dist/molecules/layout/SidebarButton/index.d.ts.map +0 -1
  250. package/dist/molecules/layout/SidebarContext.d.ts +0 -12
  251. package/dist/molecules/layout/SidebarContext.d.ts.map +0 -1
  252. package/dist/molecules/layout/index.d.ts +0 -8
  253. package/dist/molecules/layout/index.d.ts.map +0 -1
  254. package/dist/molecules/navigation/NavMenu.d.ts +0 -20
  255. package/dist/molecules/navigation/NavMenu.d.ts.map +0 -1
  256. package/dist/molecules/navigation/Pagination.d.ts +0 -14
  257. package/dist/molecules/navigation/Pagination.d.ts.map +0 -1
  258. package/dist/molecules/navigation/index.d.ts +0 -3
  259. package/dist/molecules/navigation/index.d.ts.map +0 -1
  260. package/dist/organisms/index.d.ts +0 -2
  261. package/dist/organisms/index.d.ts.map +0 -1
  262. package/dist/organisms/showcase/ComponentShowcasePage.d.ts +0 -3
  263. package/dist/organisms/showcase/ComponentShowcasePage.d.ts.map +0 -1
  264. package/dist/templates/AuthTemplate.d.ts +0 -68
  265. package/dist/templates/AuthTemplate.d.ts.map +0 -1
  266. package/dist/templates/ComponentShowcaseTemplate.d.ts +0 -53
  267. package/dist/templates/ComponentShowcaseTemplate.d.ts.map +0 -1
  268. package/dist/templates/DashboardTemplate.d.ts +0 -62
  269. package/dist/templates/DashboardTemplate.d.ts.map +0 -1
  270. package/dist/templates/DataTemplate.d.ts +0 -78
  271. package/dist/templates/DataTemplate.d.ts.map +0 -1
  272. package/dist/templates/admin/AdminCRUDTemplate.d.ts +0 -105
  273. package/dist/templates/admin/AdminCRUDTemplate.d.ts.map +0 -1
  274. package/dist/templates/admin/AdminDashboardTemplate.d.ts +0 -89
  275. package/dist/templates/admin/AdminDashboardTemplate.d.ts.map +0 -1
  276. package/dist/templates/admin/AdminDetailTemplate.d.ts +0 -132
  277. package/dist/templates/admin/AdminDetailTemplate.d.ts.map +0 -1
  278. package/dist/templates/admin/index.d.ts +0 -4
  279. package/dist/templates/admin/index.d.ts.map +0 -1
  280. package/dist/templates/factory.d.ts +0 -28
  281. package/dist/templates/factory.d.ts.map +0 -1
  282. package/dist/templates/index.d.ts +0 -7
  283. package/dist/templates/index.d.ts.map +0 -1
@@ -0,0 +1,339 @@
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
+ };
@@ -0,0 +1,236 @@
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
+ };