@pattern-stack/frontend-patterns 0.0.3 → 0.0.4

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 (154) hide show
  1. package/dist/index.es.js +1 -1
  2. package/dist/index.js +1 -0
  3. package/package.json +5 -3
  4. package/src/App.css +42 -0
  5. package/src/App.tsx +54 -0
  6. package/src/__tests__/README.md +221 -0
  7. package/src/__tests__/atoms/hooks/simple-hooks.test.ts +44 -0
  8. package/src/__tests__/atoms/ui/button.test.tsx +68 -0
  9. package/src/__tests__/atoms/utils/simple.test.ts +18 -0
  10. package/src/__tests__/atoms/utils/utils.test.ts +77 -0
  11. package/src/__tests__/features/auth/simple-auth.test.tsx +40 -0
  12. package/src/__tests__/molecules/layout/simple-layout.test.tsx +81 -0
  13. package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +167 -0
  14. package/src/__tests__/setup.ts +51 -0
  15. package/src/__tests__/utils.tsx +123 -0
  16. package/src/atoms/composed/Accordion/Accordion.tsx +271 -0
  17. package/src/atoms/composed/Accordion/index.ts +1 -0
  18. package/src/atoms/composed/Alert/Alert.tsx +132 -0
  19. package/src/atoms/composed/Alert/index.ts +1 -0
  20. package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +83 -0
  21. package/src/atoms/composed/Breadcrumb/index.ts +1 -0
  22. package/src/atoms/composed/Chart/Chart.tsx +425 -0
  23. package/src/atoms/composed/Chart/index.ts +2 -0
  24. package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +72 -0
  25. package/src/atoms/composed/ColorSwatch/index.ts +1 -0
  26. package/src/atoms/composed/DarkModeToggle.tsx +66 -0
  27. package/src/atoms/composed/DataBadge/DataBadge.tsx +81 -0
  28. package/src/atoms/composed/DataBadge/index.ts +1 -0
  29. package/src/atoms/composed/DataTable/DataTable.tsx +394 -0
  30. package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +41 -0
  31. package/src/atoms/composed/DataTable/index.ts +2 -0
  32. package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +611 -0
  33. package/src/atoms/composed/DateTimePicker/index.ts +2 -0
  34. package/src/atoms/composed/DetailedCard/DetailedCard.tsx +181 -0
  35. package/src/atoms/composed/DetailedCard/index.ts +2 -0
  36. package/src/atoms/composed/EmptyState/EmptyState.tsx +90 -0
  37. package/src/atoms/composed/EmptyState/index.ts +1 -0
  38. package/src/atoms/composed/FileUpload/FileUpload.tsx +477 -0
  39. package/src/atoms/composed/FileUpload/index.ts +2 -0
  40. package/src/atoms/composed/FormField/FormField.tsx +92 -0
  41. package/src/atoms/composed/FormField/index.ts +1 -0
  42. package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +37 -0
  43. package/src/atoms/composed/GlobalSearch/index.ts +1 -0
  44. package/src/atoms/composed/IconBadge/IconBadge.tsx +95 -0
  45. package/src/atoms/composed/IconBadge/index.ts +2 -0
  46. package/src/atoms/composed/Modal/Modal.tsx +223 -0
  47. package/src/atoms/composed/Modal/index.ts +2 -0
  48. package/src/atoms/composed/PaletteSwitcher.tsx +386 -0
  49. package/src/atoms/composed/ProgressBar/ProgressBar.tsx +116 -0
  50. package/src/atoms/composed/ProgressBar/index.ts +1 -0
  51. package/src/atoms/composed/StatCard/StatCard.tsx +219 -0
  52. package/src/atoms/composed/StatCard/index.ts +1 -0
  53. package/src/atoms/composed/StyleGuide.tsx +717 -0
  54. package/src/atoms/composed/Toast/Toast.tsx +219 -0
  55. package/src/atoms/composed/Toast/index.ts +1 -0
  56. package/src/atoms/composed/Tooltip/Tooltip.tsx +213 -0
  57. package/src/atoms/composed/Tooltip/index.ts +1 -0
  58. package/src/atoms/composed/UserAvatar/UserAvatar.tsx +139 -0
  59. package/src/atoms/composed/UserAvatar/index.ts +1 -0
  60. package/src/atoms/composed/UserMenu/UserMenu.tsx +16 -0
  61. package/src/atoms/composed/UserMenu/index.ts +1 -0
  62. package/src/atoms/composed/index.ts +29 -0
  63. package/src/atoms/hooks/useApi.ts +80 -0
  64. package/src/atoms/hooks/useHealth.ts +17 -0
  65. package/src/atoms/index.ts +13 -0
  66. package/src/atoms/services/api/client.ts +134 -0
  67. package/src/atoms/services/auth-service.ts +248 -0
  68. package/src/atoms/services/health.ts +15 -0
  69. package/src/atoms/services/index.ts +3 -0
  70. package/src/atoms/shared/config/constants.ts +17 -0
  71. package/src/atoms/shared/config/dashboard-sizes.ts +111 -0
  72. package/src/atoms/shared/config/environment.ts +10 -0
  73. package/src/atoms/shared/index.ts +4 -0
  74. package/src/atoms/shared/styles/color-palettes.css +566 -0
  75. package/src/atoms/types/auth.ts +62 -0
  76. package/src/atoms/types/generated.ts +1469 -0
  77. package/src/atoms/types/index.ts +4 -0
  78. package/src/atoms/types/loading.ts +28 -0
  79. package/src/atoms/ui/Badge.tsx +30 -0
  80. package/src/atoms/ui/ErrorBoundary.tsx +59 -0
  81. package/src/atoms/ui/Select.tsx +53 -0
  82. package/src/atoms/ui/Switch.tsx +42 -0
  83. package/src/atoms/ui/Tabs.tsx +118 -0
  84. package/src/atoms/ui/avatar.tsx +48 -0
  85. package/src/atoms/ui/button.tsx +70 -0
  86. package/src/atoms/ui/card.tsx +76 -0
  87. package/src/atoms/ui/dropdown-menu.tsx +199 -0
  88. package/src/atoms/ui/index.ts +39 -0
  89. package/src/atoms/ui/input.tsx +23 -0
  90. package/src/atoms/ui/label.tsx +23 -0
  91. package/src/atoms/ui/skeleton.tsx +13 -0
  92. package/src/atoms/ui/spinner.tsx +49 -0
  93. package/src/atoms/ui/table.tsx +116 -0
  94. package/src/atoms/utils/animations.ts +135 -0
  95. package/src/atoms/utils/tooltip-helpers.ts +140 -0
  96. package/src/atoms/utils/utils.ts +9 -0
  97. package/src/features/auth/components/LoginForm.tsx +168 -0
  98. package/src/features/auth/components/LogoutButton.tsx +19 -0
  99. package/src/features/auth/components/ProtectedRoute.tsx +60 -0
  100. package/src/features/auth/components/index.ts +4 -0
  101. package/src/features/auth/hooks/index.ts +2 -0
  102. package/src/features/auth/hooks/useAuth.tsx +205 -0
  103. package/src/features/auth/hooks/usePermissions.ts +35 -0
  104. package/src/features/auth/index.ts +2 -0
  105. package/src/features/index.ts +2 -0
  106. package/src/index.css +704 -0
  107. package/src/index.ts +13 -0
  108. package/src/main.tsx +48 -0
  109. package/src/molecules/.gitkeep +0 -0
  110. package/src/molecules/forms/FormGroup.tsx +75 -0
  111. package/src/molecules/forms/SearchInput.tsx +259 -0
  112. package/src/molecules/forms/index.ts +4 -0
  113. package/src/molecules/index.ts +4 -0
  114. package/src/molecules/layout/AppHeader/AppHeader.tsx +42 -0
  115. package/src/molecules/layout/AppHeader/index.ts +1 -0
  116. package/src/molecules/layout/AppLayout.tsx +29 -0
  117. package/src/molecules/layout/PageTemplate.tsx +87 -0
  118. package/src/molecules/layout/SectionHeader/SectionHeader.tsx +87 -0
  119. package/src/molecules/layout/SectionHeader/index.ts +1 -0
  120. package/src/molecules/layout/ShowcaseSection.tsx +57 -0
  121. package/src/molecules/layout/Sidebar.tsx +144 -0
  122. package/src/molecules/layout/SidebarButton/SidebarButton.tsx +99 -0
  123. package/src/molecules/layout/SidebarButton/index.ts +1 -0
  124. package/src/molecules/layout/SidebarContext.tsx +31 -0
  125. package/src/molecules/layout/index.ts +7 -0
  126. package/src/molecules/navigation/NavMenu.tsx +188 -0
  127. package/src/molecules/navigation/Pagination.tsx +172 -0
  128. package/src/molecules/navigation/index.ts +4 -0
  129. package/src/organisms/index.ts +5 -0
  130. package/src/organisms/showcase/ComponentShowcasePage.tsx +2496 -0
  131. package/src/organisms/showcase/index.ts +1 -0
  132. package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +242 -0
  133. package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +171 -0
  134. package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +385 -0
  135. package/src/pages/AdminShowcase/index.tsx +3 -0
  136. package/src/pages/ComponentShowcase/BadgesShowcase.tsx +188 -0
  137. package/src/pages/ComponentShowcase/CardsShowcase.tsx +392 -0
  138. package/src/pages/ComponentShowcase/PalettesShowcase.tsx +207 -0
  139. package/src/pages/ComponentShowcase/StatesShowcase.tsx +485 -0
  140. package/src/pages/ComponentShowcase/TablesShowcase.tsx +134 -0
  141. package/src/pages/ComponentShowcase/TypographyShowcase.tsx +255 -0
  142. package/src/pages/ComponentShowcase/index.tsx +188 -0
  143. package/src/pages/index.ts +2 -0
  144. package/src/templates/AuthTemplate.tsx +216 -0
  145. package/src/templates/ComponentShowcaseTemplate.tsx +173 -0
  146. package/src/templates/DashboardTemplate.tsx +232 -0
  147. package/src/templates/DataTemplate.tsx +319 -0
  148. package/src/templates/admin/AdminCRUDTemplate.tsx +630 -0
  149. package/src/templates/admin/AdminDashboardTemplate.tsx +351 -0
  150. package/src/templates/admin/AdminDetailTemplate.tsx +563 -0
  151. package/src/templates/admin/index.ts +29 -0
  152. package/src/templates/factory.tsx +169 -0
  153. package/src/templates/index.ts +37 -0
  154. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { cn } from '../../utils/utils';
3
+
4
+ export interface ColorSwatchProps {
5
+ /** CSS color value (hex, hsl, rgb, or CSS variable) */
6
+ color: string;
7
+ /** Display name for the color */
8
+ name: string;
9
+ /** Optional size variant */
10
+ size?: 'sm' | 'md' | 'lg';
11
+ /** Optional label to show below the swatch */
12
+ label?: string;
13
+ /** Additional CSS classes */
14
+ className?: string;
15
+ /** Whether the swatch is interactive */
16
+ interactive?: boolean;
17
+ /** Click handler for interactive swatches */
18
+ onClick?: () => void;
19
+ }
20
+
21
+ export const ColorSwatch: React.FC<ColorSwatchProps> = ({
22
+ color,
23
+ name,
24
+ size = 'md',
25
+ label,
26
+ className,
27
+ interactive = false,
28
+ onClick
29
+ }) => {
30
+ const sizeClasses = {
31
+ sm: 'w-8 h-8',
32
+ md: 'w-16 h-16',
33
+ lg: 'w-24 h-24'
34
+ };
35
+
36
+ const labelSizes = {
37
+ sm: 'text-xs',
38
+ md: 'text-sm',
39
+ lg: 'text-base'
40
+ };
41
+
42
+ return (
43
+ <div
44
+ className={cn(
45
+ 'flex flex-col items-center space-y-2',
46
+ interactive && 'cursor-pointer group',
47
+ className
48
+ )}
49
+ data-component-name="ColorSwatch"
50
+ onClick={onClick}
51
+ >
52
+ <div
53
+ className={cn(
54
+ 'rounded border border-border/20 flex items-center justify-center text-white font-medium',
55
+ sizeClasses[size],
56
+ interactive && 'transition-all hover:scale-105 hover:shadow group-hover:border-border'
57
+ )}
58
+ style={{ backgroundColor: color }}
59
+ title={`${name}: ${color}`}
60
+ >
61
+ {size === 'lg' && <span className="text-sm">{name}</span>}
62
+ </div>
63
+
64
+ {label && (
65
+ <div className={cn('text-center space-y-1', labelSizes[size])}>
66
+ <div className="font-medium text-foreground">{label}</div>
67
+ <div className="text-muted-foreground font-mono text-xs">{color}</div>
68
+ </div>
69
+ )}
70
+ </div>
71
+ );
72
+ };
@@ -0,0 +1 @@
1
+ export { ColorSwatch, type ColorSwatchProps } from './ColorSwatch';
@@ -0,0 +1,66 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Moon, Sun } from 'lucide-react';
3
+ import { cn } from '../utils/utils';
4
+
5
+ export const DarkModeToggle = ({ className }: { className?: string }) => {
6
+ const [isDark, setIsDark] = useState(false);
7
+
8
+ useEffect(() => {
9
+ // Check for saved preference or system preference
10
+ const savedTheme = localStorage.getItem('theme');
11
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
12
+
13
+ if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
14
+ setIsDark(true);
15
+ document.documentElement.classList.add('dark');
16
+ } else {
17
+ setIsDark(false);
18
+ document.documentElement.classList.remove('dark');
19
+ }
20
+ }, []);
21
+
22
+ const toggleDarkMode = () => {
23
+ if (isDark) {
24
+ document.documentElement.classList.remove('dark');
25
+ localStorage.setItem('theme', 'light');
26
+ setIsDark(false);
27
+ } else {
28
+ document.documentElement.classList.add('dark');
29
+ localStorage.setItem('theme', 'dark');
30
+ setIsDark(true);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <button
36
+ onClick={toggleDarkMode}
37
+ className={cn(
38
+ "relative inline-flex h-10 w-20 items-center justify-center rounded-full",
39
+ "bg-muted dark:bg-muted",
40
+ "transition-all",
41
+ "hover:bg-muted/80 dark:hover:bg-muted/80",
42
+ "focus:outline-none focus:ring-2 focus:ring-primary",
43
+ className
44
+ )}
45
+ data-component-name="DarkModeToggle"
46
+ data-state={isDark ? 'dark' : 'light'}
47
+ aria-label="Toggle dark mode"
48
+ >
49
+ <span
50
+ className={cn(
51
+ "absolute left-1 inline-flex h-8 w-8 items-center justify-center rounded-full",
52
+ "bg-background dark:bg-background",
53
+ "shadow transition-transform",
54
+ isDark && "translate-x-10"
55
+ )}
56
+ data-component-name="DarkModeToggleThumb"
57
+ >
58
+ {isDark ? (
59
+ <Moon className="h-4 w-4 text-category-4" data-component-name="DarkModeToggleIcon" />
60
+ ) : (
61
+ <Sun className="h-4 w-4 text-category-3" data-component-name="DarkModeToggleIcon" />
62
+ )}
63
+ </span>
64
+ </button>
65
+ );
66
+ };
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { cn, useTextOverflow } from '../../utils/utils';
3
+ import { Tooltip } from '../Tooltip';
4
+ import { getAnimationClasses, animationPresets } from '../../utils/animations';
5
+
6
+ export interface DataBadgeProps {
7
+ children: React.ReactNode;
8
+ variant?: 'category' | 'status';
9
+ category?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
10
+ status?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
11
+ size?: 'sm' | 'md' | 'lg';
12
+ interactive?: boolean;
13
+ onClick?: () => void;
14
+ className?: string;
15
+ }
16
+
17
+ export const DataBadge = ({
18
+ children,
19
+ variant = 'category',
20
+ category = 1,
21
+ status = 'neutral',
22
+ size = 'md',
23
+ interactive = false,
24
+ onClick,
25
+ className
26
+ }: DataBadgeProps) => {
27
+ const { ref, isOverflowing } = useTextOverflow();
28
+
29
+ const sizeClasses: Record<'sm' | 'md' | 'lg', string> = {
30
+ sm: 'px-2 py-0.5 text-xs h-5 min-w-[1.5rem] max-w-[150px]',
31
+ md: 'px-2.5 py-1 text-sm h-7 min-w-[2rem] max-w-[200px]',
32
+ lg: 'px-3 py-1.5 text-base h-9 min-w-[2.5rem] max-w-[250px]',
33
+ };
34
+
35
+ const baseClasses = cn(
36
+ 'inline-flex items-center font-medium rounded-lg border truncate',
37
+ sizeClasses[size],
38
+ interactive && [
39
+ 'cursor-pointer',
40
+ getAnimationClasses({
41
+ ...animationPresets.dataBadge,
42
+ size: size === 'lg' ? 'lg' : size === 'sm' ? 'sm' : 'md'
43
+ })
44
+ ],
45
+ className
46
+ );
47
+
48
+ const variantClasses = variant === 'category'
49
+ ? `badge-category-${category}`
50
+ : `status-${status}`;
51
+
52
+ const badge = (
53
+ <span
54
+ ref={ref as React.RefObject<HTMLSpanElement>}
55
+ className={cn(baseClasses, variantClasses, 'animate-fade-in')}
56
+ onClick={onClick}
57
+ role={interactive ? 'button' : undefined}
58
+ tabIndex={interactive ? 0 : undefined}
59
+ onKeyDown={interactive ? (e) => {
60
+ if (e.key === 'Enter' || e.key === ' ') {
61
+ e.preventDefault();
62
+ onClick?.();
63
+ }
64
+ } : undefined}
65
+ data-component-name="DataBadge"
66
+ >
67
+ {children}
68
+ </span>
69
+ );
70
+
71
+ // Show tooltip when text is truncated
72
+ if (isOverflowing && typeof children === 'string') {
73
+ return (
74
+ <Tooltip content={children} position="top" size="sm">
75
+ {badge}
76
+ </Tooltip>
77
+ );
78
+ }
79
+
80
+ return badge;
81
+ };
@@ -0,0 +1 @@
1
+ export * from './DataBadge';
@@ -0,0 +1,394 @@
1
+ import React, { useState, useMemo } from 'react';
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from '../../ui/table';
10
+ import { Input } from '../../ui/input';
11
+ import { Button } from '../../ui/button';
12
+ import { Skeleton } from '../../ui/skeleton';
13
+ import { DataBadge } from '../DataBadge';
14
+ import { cn, tooltipContent } from '../../utils/utils';
15
+ import type { IListLoadable } from '../../types';
16
+ import {
17
+ ChevronDown,
18
+ ChevronUp,
19
+ ChevronsUpDown,
20
+ Search,
21
+ X
22
+ } from 'lucide-react';
23
+
24
+ export interface Column<T> {
25
+ key: string;
26
+ header: string | React.ReactNode;
27
+ cell?: (item: T) => React.ReactNode;
28
+ sortable?: boolean;
29
+ filterable?: boolean;
30
+ width?: string;
31
+ /** Auto-render badges for status/category columns */
32
+ type?: 'status' | 'category' | 'default';
33
+ }
34
+
35
+ interface DataTableProps<T> extends IListLoadable {
36
+ data: T[];
37
+ columns: Column<T>[];
38
+ searchPlaceholder?: string;
39
+ pageSize?: number;
40
+ showPagination?: boolean;
41
+ showSearch?: boolean;
42
+ onRowClick?: (item: T) => void;
43
+ emptyMessage?: string;
44
+ className?: string;
45
+ /** Enable hover effects on table rows */
46
+ hover?: boolean;
47
+ }
48
+
49
+ type SortDirection = 'asc' | 'desc' | null;
50
+
51
+ export function DataTable<T extends Record<string, unknown>>({
52
+ data,
53
+ columns,
54
+ searchPlaceholder = "Search...",
55
+ pageSize = 10,
56
+ showPagination = true,
57
+ showSearch = true,
58
+ onRowClick,
59
+ emptyMessage = "No data available",
60
+ className = "",
61
+ hover = false,
62
+ isLoading = false,
63
+ loadingItemCount = 5
64
+ }: DataTableProps<T>) {
65
+ const [searchTerm, setSearchTerm] = useState('');
66
+ const [sortColumn, setSortColumn] = useState<string | null>(null);
67
+ const [sortDirection, setSortDirection] = useState<SortDirection>(null);
68
+ const [currentPage, setCurrentPage] = useState(1);
69
+
70
+ // Filter data based on search term
71
+ const filteredData = useMemo(() => {
72
+ if (!searchTerm) return data;
73
+
74
+ return data.filter(item => {
75
+ const searchableColumns = columns.filter(col => col.filterable !== false);
76
+ return searchableColumns.some(col => {
77
+ const value = item[col.key];
78
+ if (value === null || value === undefined) return false;
79
+ return value.toString().toLowerCase().includes(searchTerm.toLowerCase());
80
+ });
81
+ });
82
+ }, [data, searchTerm, columns]);
83
+
84
+ // Sort data
85
+ const sortedData = useMemo(() => {
86
+ if (!sortColumn || !sortDirection) return filteredData;
87
+
88
+ return [...filteredData].sort((a, b) => {
89
+ const aVal = a[sortColumn];
90
+ const bVal = b[sortColumn];
91
+
92
+ if (aVal === null || aVal === undefined) return 1;
93
+ if (bVal === null || bVal === undefined) return -1;
94
+
95
+ const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
96
+ return sortDirection === 'asc' ? comparison : -comparison;
97
+ });
98
+ }, [filteredData, sortColumn, sortDirection]);
99
+
100
+ // Paginate data
101
+ const paginatedData = useMemo(() => {
102
+ if (!showPagination) return sortedData;
103
+
104
+ const start = (currentPage - 1) * pageSize;
105
+ const end = start + pageSize;
106
+ return sortedData.slice(start, end);
107
+ }, [sortedData, currentPage, pageSize, showPagination]);
108
+
109
+ const totalPages = Math.ceil(sortedData.length / pageSize);
110
+
111
+ const handleSort = (columnKey: string) => {
112
+ if (sortColumn === columnKey) {
113
+ if (sortDirection === 'asc') {
114
+ setSortDirection('desc');
115
+ } else if (sortDirection === 'desc') {
116
+ setSortDirection(null);
117
+ setSortColumn(null);
118
+ }
119
+ } else {
120
+ setSortColumn(columnKey);
121
+ setSortDirection('asc');
122
+ }
123
+ };
124
+
125
+ const getSortIcon = (columnKey: string) => {
126
+ if (sortColumn !== columnKey) {
127
+ return <ChevronsUpDown className="w-4 h-4 text-muted-foreground" />;
128
+ }
129
+ if (sortDirection === 'asc') {
130
+ return <ChevronUp className="w-4 h-4" />;
131
+ }
132
+ return <ChevronDown className="w-4 h-4" />;
133
+ };
134
+
135
+ // Smart cell renderer that auto-detects and renders badges
136
+ const renderCell = (column: Column<T>, item: T) => {
137
+ // If custom cell renderer is provided, use it
138
+ if (column.cell) {
139
+ return column.cell(item);
140
+ }
141
+
142
+ const value = item[column.key];
143
+
144
+ // Auto-detect column type from key name if not specified
145
+ const columnType = column.type || (
146
+ column.key.toLowerCase().includes('status') ? 'status' :
147
+ column.key.toLowerCase().includes('category') ? 'category' :
148
+ 'default'
149
+ );
150
+
151
+ // Render badges for status and category columns
152
+ if (columnType === 'status' && typeof value === 'string') {
153
+ const status = value as 'success' | 'warning' | 'error' | 'info' | 'neutral';
154
+ return (
155
+ <DataBadge variant="status" status={status} size="sm">
156
+ {value.toString()}
157
+ </DataBadge>
158
+ );
159
+ }
160
+
161
+ if (columnType === 'category') {
162
+ // For category, try to map value to category number (1-8)
163
+ const categoryMap: Record<string, number> = {
164
+ 'Analytics': 1, 'Finance': 2, 'Product': 3, 'Sales': 4,
165
+ 'Marketing': 5, 'Operations': 6, 'Engineering': 7, 'Data': 8
166
+ };
167
+ const categoryNum = typeof value === 'string' ? categoryMap[value] || 1 : 1;
168
+
169
+ return (
170
+ <DataBadge variant="category" category={categoryNum as 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8} size="sm">
171
+ {value?.toString()}
172
+ </DataBadge>
173
+ );
174
+ }
175
+
176
+ // Default rendering
177
+ return value?.toString() || '-';
178
+ };
179
+
180
+ // Loading state skeleton that matches DataTable dimensions
181
+ if (isLoading) {
182
+ return (
183
+ <div className={cn("space-y-4", className)} data-component-name="DataTable">
184
+ {/* Search bar skeleton */}
185
+ {showSearch && (
186
+ <div className="flex items-center gap-2">
187
+ <div className="relative flex-1 max-w-sm">
188
+ <Skeleton className="h-10 w-full" />
189
+ </div>
190
+ </div>
191
+ )}
192
+
193
+ {/* Table skeleton */}
194
+ <div className="rounded border overflow-hidden">
195
+ <Table>
196
+ <TableHeader>
197
+ <TableRow>
198
+ {columns.map(column => (
199
+ <TableHead key={column.key} style={{ width: column.width }}>
200
+ <div className="flex items-center gap-2">
201
+ <Skeleton className="h-4 w-20" />
202
+ </div>
203
+ </TableHead>
204
+ ))}
205
+ </TableRow>
206
+ </TableHeader>
207
+ <TableBody>
208
+ {Array.from({ length: loadingItemCount }, (_, index) => (
209
+ <TableRow key={index}>
210
+ {columns.map(column => (
211
+ <TableCell key={column.key}>
212
+ <Skeleton className="h-4 w-full max-w-32" />
213
+ </TableCell>
214
+ ))}
215
+ </TableRow>
216
+ ))}
217
+ </TableBody>
218
+ </Table>
219
+ </div>
220
+
221
+ {/* Pagination skeleton */}
222
+ {showPagination && (
223
+ <div className="flex items-center justify-between">
224
+ <Skeleton className="h-4 w-48" />
225
+ <div className="flex items-center gap-2">
226
+ <Skeleton className="h-8 w-20" />
227
+ <div className="flex items-center gap-1">
228
+ {Array.from({ length: 3 }, (_, i) => (
229
+ <Skeleton key={i} className="h-8 w-8" />
230
+ ))}
231
+ </div>
232
+ <Skeleton className="h-8 w-16" />
233
+ </div>
234
+ </div>
235
+ )}
236
+ </div>
237
+ );
238
+ }
239
+
240
+ return (
241
+ <div className={cn("space-y-4", className)} data-component-name="DataTable">
242
+ {/* Search bar */}
243
+ {showSearch && (
244
+ <div className="flex items-center gap-2">
245
+ <div className="relative flex-1 max-w-sm">
246
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" data-component-name="DataTableSearch" />
247
+ <Input
248
+ type="text"
249
+ placeholder={searchPlaceholder}
250
+ value={searchTerm}
251
+ onChange={(e) => {
252
+ setSearchTerm(e.target.value);
253
+ setCurrentPage(1);
254
+ }}
255
+ className="pl-10 pr-10"
256
+ />
257
+ {searchTerm && (
258
+ <Button
259
+ onClick={() => {
260
+ setSearchTerm('');
261
+ setCurrentPage(1);
262
+ }}
263
+ variant="ghost"
264
+ size="icon"
265
+ className="absolute right-1 top-1/2 transform -translate-y-1/2 h-8 w-8"
266
+ tooltip={tooltipContent.clearSearch}
267
+ >
268
+ <X className="w-4 h-4" data-component-name="DataTableClear" />
269
+ </Button>
270
+ )}
271
+ </div>
272
+ {searchTerm && (
273
+ <div className="text-muted-foreground text-sm">
274
+ {sortedData.length} result{sortedData.length !== 1 ? 's' : ''}
275
+ </div>
276
+ )}
277
+ </div>
278
+ )}
279
+
280
+ {/* Table */}
281
+ <div className="rounded border overflow-hidden">
282
+ <Table>
283
+ <TableHeader>
284
+ <TableRow>
285
+ {columns.map(column => (
286
+ <TableHead
287
+ key={column.key}
288
+ style={{ width: column.width }}
289
+ className={column.sortable !== false && typeof column.header === 'string' ? 'cursor-pointer select-none' : ''}
290
+ onClick={() => column.sortable !== false && typeof column.header === 'string' && handleSort(column.key)}
291
+ >
292
+ <div className="flex items-center gap-2">
293
+ {typeof column.header === 'string' ? (
294
+ <>
295
+ <span>{column.header}</span>
296
+ {column.sortable !== false && getSortIcon(column.key)}
297
+ </>
298
+ ) : (
299
+ column.header
300
+ )}
301
+ </div>
302
+ </TableHead>
303
+ ))}
304
+ </TableRow>
305
+ </TableHeader>
306
+ <TableBody>
307
+ {paginatedData.length === 0 ? (
308
+ <TableRow>
309
+ <TableCell
310
+ colSpan={columns.length}
311
+ className="text-center py-8 text-muted-foreground"
312
+ >
313
+ {emptyMessage}
314
+ </TableCell>
315
+ </TableRow>
316
+ ) : (
317
+ paginatedData.map((item, index) => (
318
+ <TableRow
319
+ key={index}
320
+ className={cn(
321
+ onRowClick && 'cursor-pointer',
322
+ (hover || onRowClick) && 'hover:bg-muted/50'
323
+ )}
324
+ onClick={() => onRowClick?.(item)}
325
+ >
326
+ {columns.map(column => (
327
+ <TableCell key={column.key}>
328
+ {renderCell(column, item)}
329
+ </TableCell>
330
+ ))}
331
+ </TableRow>
332
+ ))
333
+ )}
334
+ </TableBody>
335
+ </Table>
336
+ </div>
337
+
338
+ {/* Pagination */}
339
+ {showPagination && totalPages > 1 && (
340
+ <div className="flex items-center justify-between">
341
+ <div className="text-muted-foreground text-sm">
342
+ Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, sortedData.length)} of {sortedData.length} entries
343
+ </div>
344
+ <div className="flex items-center gap-2">
345
+ <Button
346
+ variant="outline"
347
+ size="sm"
348
+ onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
349
+ disabled={currentPage === 1}
350
+ tooltip={tooltipContent.previous}
351
+ >
352
+ Previous
353
+ </Button>
354
+ <div className="flex items-center gap-1">
355
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
356
+ let pageNum;
357
+ if (totalPages <= 5) {
358
+ pageNum = i + 1;
359
+ } else if (currentPage <= 3) {
360
+ pageNum = i + 1;
361
+ } else if (currentPage >= totalPages - 2) {
362
+ pageNum = totalPages - 4 + i;
363
+ } else {
364
+ pageNum = currentPage - 2 + i;
365
+ }
366
+
367
+ return (
368
+ <Button
369
+ key={pageNum}
370
+ variant={currentPage === pageNum ? "default" : "outline"}
371
+ size="sm"
372
+ onClick={() => setCurrentPage(pageNum)}
373
+ className="w-8 h-8"
374
+ >
375
+ {pageNum}
376
+ </Button>
377
+ );
378
+ })}
379
+ </div>
380
+ <Button
381
+ variant="outline"
382
+ size="sm"
383
+ onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
384
+ disabled={currentPage === totalPages}
385
+ tooltip={tooltipContent.next}
386
+ >
387
+ Next
388
+ </Button>
389
+ </div>
390
+ </div>
391
+ )}
392
+ </div>
393
+ );
394
+ }
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { TableCell } from '../../ui/table';
3
+ import { Tooltip } from '../Tooltip';
4
+ import { useTextOverflow } from '../../utils/utils';
5
+
6
+ interface TableCellWithTooltipProps {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ /** Maximum width before truncating */
10
+ maxWidth?: string;
11
+ }
12
+
13
+ export const TableCellWithTooltip: React.FC<TableCellWithTooltipProps> = ({
14
+ children,
15
+ className,
16
+ maxWidth = '200px'
17
+ }) => {
18
+ const { ref, isOverflowing } = useTextOverflow();
19
+
20
+ const cellContent = (
21
+ <div
22
+ ref={ref as React.RefObject<HTMLDivElement>}
23
+ className="truncate"
24
+ style={{ maxWidth }}
25
+ >
26
+ {children}
27
+ </div>
28
+ );
29
+
30
+ return (
31
+ <TableCell className={className}>
32
+ {isOverflowing && typeof children === 'string' ? (
33
+ <Tooltip content={children} position="top" size="sm">
34
+ {cellContent}
35
+ </Tooltip>
36
+ ) : (
37
+ cellContent
38
+ )}
39
+ </TableCell>
40
+ );
41
+ };
@@ -0,0 +1,2 @@
1
+ export * from './DataTable';
2
+ export * from './TableCellWithTooltip';