@pattern-stack/frontend-patterns 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/package.json +3 -5
- package/src/App.css +0 -42
- package/src/App.tsx +0 -54
- package/src/__tests__/README.md +0 -221
- package/src/__tests__/atoms/hooks/simple-hooks.test.ts +0 -44
- package/src/__tests__/atoms/ui/button.test.tsx +0 -68
- package/src/__tests__/atoms/utils/simple.test.ts +0 -18
- package/src/__tests__/atoms/utils/utils.test.ts +0 -77
- package/src/__tests__/features/auth/simple-auth.test.tsx +0 -40
- package/src/__tests__/molecules/layout/simple-layout.test.tsx +0 -81
- package/src/__tests__/organisms/showcase/simple-showcase.test.tsx +0 -167
- package/src/__tests__/setup.ts +0 -51
- package/src/__tests__/utils.tsx +0 -123
- package/src/atoms/composed/Accordion/Accordion.tsx +0 -271
- package/src/atoms/composed/Accordion/index.ts +0 -1
- package/src/atoms/composed/Alert/Alert.tsx +0 -132
- package/src/atoms/composed/Alert/index.ts +0 -1
- package/src/atoms/composed/Breadcrumb/Breadcrumb.tsx +0 -83
- package/src/atoms/composed/Breadcrumb/index.ts +0 -1
- package/src/atoms/composed/Chart/Chart.tsx +0 -425
- package/src/atoms/composed/Chart/index.ts +0 -2
- package/src/atoms/composed/ColorSwatch/ColorSwatch.tsx +0 -72
- package/src/atoms/composed/ColorSwatch/index.ts +0 -1
- package/src/atoms/composed/DarkModeToggle.tsx +0 -66
- package/src/atoms/composed/DataBadge/DataBadge.tsx +0 -81
- package/src/atoms/composed/DataBadge/index.ts +0 -1
- package/src/atoms/composed/DataTable/DataTable.tsx +0 -394
- package/src/atoms/composed/DataTable/TableCellWithTooltip.tsx +0 -41
- package/src/atoms/composed/DataTable/index.ts +0 -2
- package/src/atoms/composed/DateTimePicker/DateTimePicker.tsx +0 -611
- package/src/atoms/composed/DateTimePicker/index.ts +0 -2
- package/src/atoms/composed/DetailedCard/DetailedCard.tsx +0 -181
- package/src/atoms/composed/DetailedCard/index.ts +0 -2
- package/src/atoms/composed/EmptyState/EmptyState.tsx +0 -90
- package/src/atoms/composed/EmptyState/index.ts +0 -1
- package/src/atoms/composed/FileUpload/FileUpload.tsx +0 -477
- package/src/atoms/composed/FileUpload/index.ts +0 -2
- package/src/atoms/composed/FormField/FormField.tsx +0 -92
- package/src/atoms/composed/FormField/index.ts +0 -1
- package/src/atoms/composed/GlobalSearch/GlobalSearch.tsx +0 -37
- package/src/atoms/composed/GlobalSearch/index.ts +0 -1
- package/src/atoms/composed/IconBadge/IconBadge.tsx +0 -95
- package/src/atoms/composed/IconBadge/index.ts +0 -2
- package/src/atoms/composed/Modal/Modal.tsx +0 -223
- package/src/atoms/composed/Modal/index.ts +0 -2
- package/src/atoms/composed/PaletteSwitcher.tsx +0 -386
- package/src/atoms/composed/ProgressBar/ProgressBar.tsx +0 -116
- package/src/atoms/composed/ProgressBar/index.ts +0 -1
- package/src/atoms/composed/StatCard/StatCard.tsx +0 -219
- package/src/atoms/composed/StatCard/index.ts +0 -1
- package/src/atoms/composed/StyleGuide.tsx +0 -717
- package/src/atoms/composed/Toast/Toast.tsx +0 -219
- package/src/atoms/composed/Toast/index.ts +0 -1
- package/src/atoms/composed/Tooltip/Tooltip.tsx +0 -213
- package/src/atoms/composed/Tooltip/index.ts +0 -1
- package/src/atoms/composed/UserAvatar/UserAvatar.tsx +0 -139
- package/src/atoms/composed/UserAvatar/index.ts +0 -1
- package/src/atoms/composed/UserMenu/UserMenu.tsx +0 -16
- package/src/atoms/composed/UserMenu/index.ts +0 -1
- package/src/atoms/composed/index.ts +0 -29
- package/src/atoms/hooks/useApi.ts +0 -80
- package/src/atoms/hooks/useHealth.ts +0 -17
- package/src/atoms/index.ts +0 -13
- package/src/atoms/services/api/client.ts +0 -134
- package/src/atoms/services/auth-service.ts +0 -248
- package/src/atoms/services/health.ts +0 -15
- package/src/atoms/services/index.ts +0 -3
- package/src/atoms/shared/config/constants.ts +0 -17
- package/src/atoms/shared/config/dashboard-sizes.ts +0 -111
- package/src/atoms/shared/config/environment.ts +0 -10
- package/src/atoms/shared/index.ts +0 -4
- package/src/atoms/shared/styles/color-palettes.css +0 -566
- package/src/atoms/types/auth.ts +0 -62
- package/src/atoms/types/generated.ts +0 -1469
- package/src/atoms/types/index.ts +0 -4
- package/src/atoms/types/loading.ts +0 -28
- package/src/atoms/ui/Badge.tsx +0 -30
- package/src/atoms/ui/ErrorBoundary.tsx +0 -59
- package/src/atoms/ui/Select.tsx +0 -53
- package/src/atoms/ui/Switch.tsx +0 -42
- package/src/atoms/ui/Tabs.tsx +0 -118
- package/src/atoms/ui/avatar.tsx +0 -48
- package/src/atoms/ui/button.tsx +0 -70
- package/src/atoms/ui/card.tsx +0 -76
- package/src/atoms/ui/dropdown-menu.tsx +0 -199
- package/src/atoms/ui/index.ts +0 -39
- package/src/atoms/ui/input.tsx +0 -23
- package/src/atoms/ui/label.tsx +0 -23
- package/src/atoms/ui/skeleton.tsx +0 -13
- package/src/atoms/ui/spinner.tsx +0 -49
- package/src/atoms/ui/table.tsx +0 -116
- package/src/atoms/utils/animations.ts +0 -135
- package/src/atoms/utils/tooltip-helpers.ts +0 -140
- package/src/atoms/utils/utils.ts +0 -9
- package/src/features/auth/components/LoginForm.tsx +0 -168
- package/src/features/auth/components/LogoutButton.tsx +0 -19
- package/src/features/auth/components/ProtectedRoute.tsx +0 -60
- package/src/features/auth/components/index.ts +0 -4
- package/src/features/auth/hooks/index.ts +0 -2
- package/src/features/auth/hooks/useAuth.tsx +0 -205
- package/src/features/auth/hooks/usePermissions.ts +0 -35
- package/src/features/auth/index.ts +0 -2
- package/src/features/index.ts +0 -2
- package/src/index.css +0 -704
- package/src/index.ts +0 -13
- package/src/main.tsx +0 -48
- package/src/molecules/.gitkeep +0 -0
- package/src/molecules/forms/FormGroup.tsx +0 -75
- package/src/molecules/forms/SearchInput.tsx +0 -259
- package/src/molecules/forms/index.ts +0 -4
- package/src/molecules/index.ts +0 -4
- package/src/molecules/layout/AppHeader/AppHeader.tsx +0 -42
- package/src/molecules/layout/AppHeader/index.ts +0 -1
- package/src/molecules/layout/AppLayout.tsx +0 -29
- package/src/molecules/layout/PageTemplate.tsx +0 -87
- package/src/molecules/layout/SectionHeader/SectionHeader.tsx +0 -87
- package/src/molecules/layout/SectionHeader/index.ts +0 -1
- package/src/molecules/layout/ShowcaseSection.tsx +0 -57
- package/src/molecules/layout/Sidebar.tsx +0 -144
- package/src/molecules/layout/SidebarButton/SidebarButton.tsx +0 -99
- package/src/molecules/layout/SidebarButton/index.ts +0 -1
- package/src/molecules/layout/SidebarContext.tsx +0 -31
- package/src/molecules/layout/index.ts +0 -7
- package/src/molecules/navigation/NavMenu.tsx +0 -188
- package/src/molecules/navigation/Pagination.tsx +0 -172
- package/src/molecules/navigation/index.ts +0 -4
- package/src/organisms/index.ts +0 -5
- package/src/organisms/showcase/ComponentShowcasePage.tsx +0 -2496
- package/src/organisms/showcase/index.ts +0 -1
- package/src/pages/AdminShowcase/AdminCRUDShowcase.tsx +0 -242
- package/src/pages/AdminShowcase/AdminDashboardShowcase.tsx +0 -171
- package/src/pages/AdminShowcase/AdminDetailShowcase.tsx +0 -385
- package/src/pages/AdminShowcase/index.tsx +0 -3
- package/src/pages/ComponentShowcase/BadgesShowcase.tsx +0 -188
- package/src/pages/ComponentShowcase/CardsShowcase.tsx +0 -392
- package/src/pages/ComponentShowcase/PalettesShowcase.tsx +0 -207
- package/src/pages/ComponentShowcase/StatesShowcase.tsx +0 -485
- package/src/pages/ComponentShowcase/TablesShowcase.tsx +0 -134
- package/src/pages/ComponentShowcase/TypographyShowcase.tsx +0 -255
- package/src/pages/ComponentShowcase/index.tsx +0 -188
- package/src/pages/index.ts +0 -2
- package/src/templates/AuthTemplate.tsx +0 -216
- package/src/templates/ComponentShowcaseTemplate.tsx +0 -173
- package/src/templates/DashboardTemplate.tsx +0 -232
- package/src/templates/DataTemplate.tsx +0 -319
- package/src/templates/admin/AdminCRUDTemplate.tsx +0 -630
- package/src/templates/admin/AdminDashboardTemplate.tsx +0 -351
- package/src/templates/admin/AdminDetailTemplate.tsx +0 -563
- package/src/templates/admin/index.ts +0 -29
- package/src/templates/factory.tsx +0 -169
- package/src/templates/index.ts +0 -37
- package/src/vite-env.d.ts +0 -1
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { ColorSwatch, type ColorSwatchProps } from './ColorSwatch';
|
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,81 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './DataBadge';
|
|
@@ -1,394 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
};
|