mirror of
https://github.com/hanzoai/o11y-components.git
synced 2026-10-30 14:54:15 +00:00
A version written into a manifest is written into every manifest that wants it, which is how eslint came to exist in a hundred and four forms across this estate and how a package came to resolve twice at run time. The catalog moves the number out of the manifest: a manifest names the package, the workspace names the version. Four are held below their latest because the newer release cannot be used, and the reason is recorded rather than the version alone: @noble/hashes 2 renamed every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels 3 points its types condition at the bundle.
2394 lines
61 KiB
TypeScript
2394 lines
61 KiB
TypeScript
import { CircleAlert, CircleCheck, CircleX, Clock, Eye, Pencil, Trash2 } from '@signozhq/icons';
|
|
import {
|
|
Badge,
|
|
type BadgeColor,
|
|
DataTable as BaseDataTable,
|
|
Button,
|
|
ButtonColor,
|
|
type ColumnDef,
|
|
type Row,
|
|
} from '@signozhq/ui';
|
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
|
|
type IconComponent = React.ComponentType<{
|
|
className?: string;
|
|
size?: number;
|
|
style?: React.CSSProperties;
|
|
}>;
|
|
|
|
import * as React from 'react';
|
|
|
|
// Create a properly typed wrapper component
|
|
const DataTable = <TData,>({
|
|
columns,
|
|
data,
|
|
tableId,
|
|
...props
|
|
}: React.ComponentProps<typeof BaseDataTable<TData, unknown>>) => (
|
|
<BaseDataTable columns={columns} data={data} tableId={tableId} {...props} />
|
|
);
|
|
|
|
// Define the type for our data
|
|
type User = {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
role: 'admin' | 'user' | 'moderator' | 'guest';
|
|
status: 'active' | 'inactive' | 'pending' | 'suspended';
|
|
lastLogin: string;
|
|
createdAt: string;
|
|
avatar?: string;
|
|
department: string;
|
|
salary: number;
|
|
performance: number;
|
|
};
|
|
|
|
// Enhanced sample data
|
|
const users: User[] = [
|
|
{
|
|
id: '1',
|
|
name: 'Sarah Johnson',
|
|
email: '[email protected]',
|
|
role: 'admin',
|
|
status: 'active',
|
|
lastLogin: '2024-01-15T10:30:00Z',
|
|
createdAt: '2023-03-15',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Engineering',
|
|
salary: 95000,
|
|
performance: 95,
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'Michael Chen',
|
|
email: '[email protected]',
|
|
role: 'user',
|
|
status: 'active',
|
|
lastLogin: '2024-01-14T15:45:00Z',
|
|
createdAt: '2023-06-20',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Marketing',
|
|
salary: 75000,
|
|
performance: 88,
|
|
},
|
|
{
|
|
id: '3',
|
|
name: 'Emily Rodriguez',
|
|
email: '[email protected]',
|
|
role: 'moderator',
|
|
status: 'pending',
|
|
lastLogin: '2024-01-10T09:15:00Z',
|
|
createdAt: '2023-09-10',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Support',
|
|
salary: 65000,
|
|
performance: 92,
|
|
},
|
|
{
|
|
id: '4',
|
|
name: 'David Kim',
|
|
email: '[email protected]',
|
|
role: 'user',
|
|
status: 'inactive',
|
|
lastLogin: '2024-01-05T14:20:00Z',
|
|
createdAt: '2023-11-05',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Sales',
|
|
salary: 80000,
|
|
performance: 78,
|
|
},
|
|
{
|
|
id: '5',
|
|
name: 'Lisa Wang',
|
|
email: '[email protected]',
|
|
role: 'admin',
|
|
status: 'active',
|
|
lastLogin: '2024-01-15T11:00:00Z',
|
|
createdAt: '2023-01-20',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Product',
|
|
salary: 110000,
|
|
performance: 98,
|
|
},
|
|
{
|
|
id: '6',
|
|
name: 'James Wilson',
|
|
email: '[email protected]',
|
|
role: 'guest',
|
|
status: 'suspended',
|
|
lastLogin: '2024-01-02T16:30:00Z',
|
|
createdAt: '2023-12-01',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Engineering',
|
|
salary: 85000,
|
|
performance: 65,
|
|
},
|
|
{
|
|
id: '7',
|
|
name: 'John Doe',
|
|
email: '[email protected]',
|
|
role: 'admin',
|
|
status: 'active',
|
|
lastLogin: '2024-01-02T16:30:00Z',
|
|
createdAt: '2023-12-01',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Engineering',
|
|
salary: 85000,
|
|
performance: 65,
|
|
},
|
|
{
|
|
id: '8',
|
|
name: 'John Doe',
|
|
email: '[email protected]',
|
|
role: 'admin',
|
|
status: 'active',
|
|
lastLogin: '2024-01-02T16:30:00Z',
|
|
createdAt: '2023-12-01',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Engineering',
|
|
salary: 85000,
|
|
performance: 65,
|
|
},
|
|
{
|
|
id: '9',
|
|
name: 'John Doe',
|
|
email: '[email protected]',
|
|
role: 'admin',
|
|
status: 'active',
|
|
lastLogin: '2024-01-02T16:30:00Z',
|
|
createdAt: '2023-12-01',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Engineering',
|
|
salary: 85000,
|
|
performance: 65,
|
|
},
|
|
{
|
|
id: '10',
|
|
name: 'John Doe',
|
|
email: '[email protected]',
|
|
role: 'admin',
|
|
status: 'active',
|
|
lastLogin: '2024-01-02T16:30:00Z',
|
|
createdAt: '2023-12-01',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Engineering',
|
|
salary: 85000,
|
|
performance: 65,
|
|
},
|
|
{
|
|
id: '11',
|
|
name: 'John Doe',
|
|
email: '[email protected]',
|
|
role: 'admin',
|
|
status: 'active',
|
|
lastLogin: '2024-01-02T16:30:00Z',
|
|
createdAt: '2023-12-01',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Engineering',
|
|
salary: 85000,
|
|
performance: 65,
|
|
},
|
|
{
|
|
id: '12',
|
|
name: 'John Doe',
|
|
email: '[email protected]',
|
|
role: 'admin',
|
|
status: 'active',
|
|
lastLogin: '2024-01-02T16:30:00Z',
|
|
createdAt: '2023-12-01',
|
|
avatar:
|
|
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=32&h=32&fit=crop&crop=face',
|
|
department: 'Engineering',
|
|
salary: 85000,
|
|
performance: 65,
|
|
},
|
|
];
|
|
|
|
// Enhanced columns with better visual presentation and individual width constraints
|
|
const enhancedColumns: ColumnDef<User>[] = [
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Employee',
|
|
size: 250, // Default width
|
|
minSize: 200, // Minimum width
|
|
maxSize: 350, // Maximum width
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const user = row.original;
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div
|
|
style={{
|
|
backgroundImage: 'linear-gradient(to bottom right in oklab, #3080ff, #9810fa)',
|
|
height: 32,
|
|
width: 32,
|
|
borderRadius: 9999,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
lineHeight: 1.33333,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{user.name
|
|
.split(' ')
|
|
.map((n: string) => n[0])
|
|
.join('')}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>{user.name}</span>
|
|
<span style={{ fontSize: 12, lineHeight: 1.33333, color: 'var(--muted-foreground)' }}>
|
|
{user.email}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
size: 120, // Default width
|
|
minSize: 100, // Minimum width
|
|
maxSize: 150, // Maximum width
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const role = row.getValue('role') as User['role'];
|
|
const roleMap: Record<User['role'], { label: string; color: BadgeColor }> = {
|
|
admin: {
|
|
label: 'Admin',
|
|
color: 'sakura',
|
|
},
|
|
user: {
|
|
label: 'User',
|
|
color: 'robin',
|
|
},
|
|
moderator: {
|
|
label: 'Moderator',
|
|
color: 'sienna',
|
|
},
|
|
guest: {
|
|
label: 'Guest',
|
|
color: 'secondary',
|
|
},
|
|
};
|
|
const roleInfo = roleMap[role];
|
|
return (
|
|
<Badge variant="outline" color={roleInfo.color}>
|
|
{roleInfo.label}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: 'Status',
|
|
size: 140, // Default width
|
|
minSize: 120, // Minimum width
|
|
maxSize: 180, // Maximum width
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const status = row.getValue('status') as User['status'];
|
|
const statusMap: Record<
|
|
User['status'],
|
|
{ label: string; icon: IconComponent; color: BadgeColor }
|
|
> = {
|
|
active: {
|
|
label: 'Active',
|
|
icon: CircleCheck,
|
|
color: 'success',
|
|
},
|
|
inactive: {
|
|
label: 'Inactive',
|
|
icon: CircleX,
|
|
color: 'error',
|
|
},
|
|
pending: {
|
|
label: 'Pending',
|
|
icon: Clock,
|
|
color: 'warning',
|
|
},
|
|
suspended: {
|
|
label: 'Suspended',
|
|
icon: CircleAlert,
|
|
color: 'secondary',
|
|
},
|
|
};
|
|
const statusInfo = statusMap[status];
|
|
const Icon = statusInfo.icon;
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<Icon style={{ height: 16, width: 16 }} />
|
|
<Badge variant="outline" color={statusInfo.color}>
|
|
{statusInfo.label}
|
|
</Badge>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'department',
|
|
header: 'Department',
|
|
size: 150, // Default width
|
|
minSize: 120, // Minimum width
|
|
maxSize: 200, // Maximum width
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const department = row.getValue('department') as string;
|
|
return (
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>{department}</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'salary',
|
|
header: 'Salary',
|
|
size: 120, // Default width
|
|
minSize: 100, // Minimum width
|
|
maxSize: 150, // Maximum width
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const salary = parseFloat(row.getValue('salary') as string);
|
|
const formatted = new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(salary);
|
|
return (
|
|
<div style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857, color: '#008138' }}>
|
|
{formatted}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'performance',
|
|
header: 'Performance',
|
|
size: 180, // Default width
|
|
minSize: 150, // Minimum width
|
|
maxSize: 250, // Maximum width
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const performance = parseFloat(row.getValue('performance') as string);
|
|
const getPerformanceColor = (score: number) => {
|
|
if (score >= 90) return 'var(--accent-forest)';
|
|
if (score >= 80) return 'var(--accent-primary)';
|
|
if (score >= 70) return 'var(--accent-amber)';
|
|
return 'var(--accent-cherry)';
|
|
};
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<div style={{ flex: 1, backgroundColor: '#e5e7eb', borderRadius: 9999, height: 8 }}>
|
|
<div
|
|
style={{
|
|
height: 8,
|
|
borderRadius: 9999,
|
|
backgroundColor: getPerformanceColor(performance),
|
|
width: `${performance}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
<span
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
fontWeight: 500,
|
|
color: getPerformanceColor(performance),
|
|
}}
|
|
>
|
|
{performance}%
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'lastLogin',
|
|
header: 'Last Login',
|
|
size: 140, // Default width
|
|
minSize: 120, // Minimum width
|
|
maxSize: 180, // Maximum width
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const date = new Date(row.getValue('lastLogin') as string);
|
|
const formatted = date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
return (
|
|
<span style={{ fontSize: 14, lineHeight: 1.42857, color: 'var(--muted-foreground)' }}>
|
|
{formatted}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'actions',
|
|
header: 'Actions',
|
|
cell: () => {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Eye style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Pencil style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.Destructive}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Trash2 style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
// Simple columns for reorder testing
|
|
const simpleColumns: ColumnDef<User>[] = [
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Name',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<div
|
|
style={{
|
|
backgroundImage: 'linear-gradient(to bottom right in oklab, #3080ff, #9810fa)',
|
|
height: 24,
|
|
width: 24,
|
|
borderRadius: 9999,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
lineHeight: 1.33333,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{row.original.name
|
|
.split(' ')
|
|
.map((n: string) => n[0])
|
|
.join('')}
|
|
</div>
|
|
<span style={{ fontWeight: 500 }}>{row.original.name}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'email',
|
|
header: 'Email',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<span style={{ fontSize: 14, lineHeight: 1.42857, color: 'var(--muted-foreground)' }}>
|
|
{row.original.email}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const role = row.original.role;
|
|
const roleMap: Record<User['role'], { label: string; color: BadgeColor }> = {
|
|
admin: { label: 'Admin', color: 'sakura' },
|
|
user: { label: 'User', color: 'robin' },
|
|
moderator: {
|
|
label: 'Moderator',
|
|
color: 'sienna',
|
|
},
|
|
guest: { label: 'Guest', color: 'secondary' },
|
|
};
|
|
const roleInfo = roleMap[role];
|
|
return <Badge color={roleInfo.color}>{roleInfo.label}</Badge>;
|
|
},
|
|
},
|
|
];
|
|
|
|
// Story: Basic DataTable with essential features
|
|
export const Basic: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
Employee Directory
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
A basic data table with sorting, filtering, and pagination capabilities.
|
|
</p>
|
|
<DataTable {...args} />
|
|
</div>
|
|
</div>
|
|
),
|
|
args: {
|
|
columns: enhancedColumns,
|
|
data: users,
|
|
tableId: 'basic-employees-table',
|
|
enableSorting: true,
|
|
enableFiltering: true,
|
|
enableGlobalFilter: true,
|
|
enableColumnReordering: false,
|
|
enableColumnResizing: false,
|
|
enableColumnPinning: false,
|
|
enableRowSelection: false,
|
|
enablePagination: true,
|
|
pageSize: 5,
|
|
showHeaders: true,
|
|
},
|
|
};
|
|
|
|
// Story: Advanced DataTable with all features
|
|
export const Advanced: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
Advanced Employee Management
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
Full-featured data table with column reordering, resizing, pinning, row selection, and
|
|
more.
|
|
</p>
|
|
<DataTable {...args} />
|
|
</div>
|
|
</div>
|
|
),
|
|
args: {
|
|
columns: enhancedColumns,
|
|
data: users,
|
|
tableId: 'advanced-employees-table',
|
|
enableSorting: true,
|
|
enableFiltering: true,
|
|
enableGlobalFilter: true,
|
|
enableColumnReordering: true,
|
|
enableColumnResizing: true,
|
|
enableColumnPinning: true,
|
|
enableRowSelection: true,
|
|
enablePagination: true,
|
|
pageSize: 5,
|
|
showHeaders: true,
|
|
},
|
|
};
|
|
|
|
// Story: Column Reordering Demo
|
|
export const ColumnReordering: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
Column Reordering Demo
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
Drag and drop column headers to reorder them. Try dragging the "Name" column to
|
|
different positions.
|
|
</p>
|
|
<DataTable {...args} />
|
|
</div>
|
|
</div>
|
|
),
|
|
args: {
|
|
columns: simpleColumns,
|
|
data: users,
|
|
tableId: 'reorder-demo-table',
|
|
enableColumnReordering: true,
|
|
enableColumnResizing: false,
|
|
enableSorting: false,
|
|
enableFiltering: false,
|
|
enableGlobalFilter: false,
|
|
enableColumnPinning: false,
|
|
enableRowSelection: false,
|
|
enablePagination: false,
|
|
showHeaders: true,
|
|
},
|
|
};
|
|
|
|
// Story: Row Selection Demo
|
|
export const RowSelection: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
Row Selection Demo
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
Select individual rows or use the header checkbox to select all rows. Selected rows are
|
|
highlighted.
|
|
</p>
|
|
<DataTable {...args} />
|
|
</div>
|
|
</div>
|
|
),
|
|
args: {
|
|
columns: enhancedColumns,
|
|
data: users,
|
|
tableId: 'selection-demo-table',
|
|
enableSorting: true,
|
|
enableFiltering: true,
|
|
enableGlobalFilter: false,
|
|
enableColumnReordering: false,
|
|
enableColumnResizing: false,
|
|
enableColumnPinning: false,
|
|
enableRowSelection: true,
|
|
enablePagination: true,
|
|
pageSize: 5,
|
|
showHeaders: true,
|
|
},
|
|
};
|
|
|
|
// Story: Compact View
|
|
export const Compact: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
Compact Employee List
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
A compact view with essential information only, perfect for mobile or space-constrained
|
|
layouts.
|
|
</p>
|
|
<DataTable {...args} />
|
|
</div>
|
|
</div>
|
|
),
|
|
args: {
|
|
columns: [
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Employee',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<div
|
|
style={{
|
|
backgroundImage: 'linear-gradient(to bottom right in oklab, #3080ff, #9810fa)',
|
|
height: 24,
|
|
width: 24,
|
|
borderRadius: 9999,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
lineHeight: 1.33333,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{row.original.name
|
|
.split(' ')
|
|
.map((n: string) => n[0])
|
|
.join('')}
|
|
</div>
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>
|
|
{row.original.name}
|
|
</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const role = row.original.role;
|
|
const roleMap: Record<User['role'], { label: string; color: BadgeColor }> = {
|
|
admin: { label: 'Admin', color: 'sakura' },
|
|
user: { label: 'User', color: 'robin' },
|
|
moderator: { label: 'Mod', color: 'sienna' },
|
|
guest: { label: 'Guest', color: 'secondary' },
|
|
};
|
|
const roleInfo = roleMap[role];
|
|
return <Badge color={roleInfo.color}>{roleInfo.label}</Badge>;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: 'Status',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const status = row.original.status;
|
|
const statusMap: Record<User['status'], { icon: IconComponent; color: string }> = {
|
|
active: { icon: CircleCheck, color: 'var(--accent-forest)' },
|
|
inactive: { icon: CircleX, color: 'var(--accent-cherry)' },
|
|
pending: { icon: Clock, color: 'var(--accent-amber)' },
|
|
suspended: { icon: CircleAlert, color: 'var(--l3-foreground)' },
|
|
};
|
|
const statusInfo = statusMap[status];
|
|
const Icon = statusInfo.icon;
|
|
return <Icon size={16} style={{ color: statusInfo.color }} />;
|
|
},
|
|
},
|
|
],
|
|
data: users,
|
|
tableId: 'compact-employees-table',
|
|
enableSorting: true,
|
|
enableFiltering: false,
|
|
enableGlobalFilter: false,
|
|
enableColumnReordering: false,
|
|
enableColumnResizing: false,
|
|
enableColumnPinning: false,
|
|
enableRowSelection: false,
|
|
enablePagination: true,
|
|
pageSize: 10,
|
|
showHeaders: true,
|
|
},
|
|
};
|
|
|
|
// Story: Column Resizing Demo
|
|
export const ColumnResizing: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
Column Resizing Demo
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
Hover over column headers to see the resize handle. Drag the right edge of column headers
|
|
to resize them. Double-click the resize handle to reset column width.
|
|
</p>
|
|
<DataTable {...args} />
|
|
</div>
|
|
</div>
|
|
),
|
|
args: {
|
|
columns: [
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Employee Name',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<div
|
|
style={{
|
|
backgroundImage: 'linear-gradient(to bottom right in oklab, #3080ff, #9810fa)',
|
|
height: 24,
|
|
width: 24,
|
|
borderRadius: 9999,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
lineHeight: 1.33333,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{row.original.name
|
|
.split(' ')
|
|
.map((n: string) => n[0])
|
|
.join('')}
|
|
</div>
|
|
<span style={{ fontWeight: 500 }}>{row.original.name}</span>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'email',
|
|
header: 'Email Address',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<span style={{ fontSize: 14, lineHeight: 1.42857, color: 'var(--muted-foreground)' }}>
|
|
{row.original.email}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const role = row.original.role;
|
|
const roleMap: Record<User['role'], { label: string; color: BadgeColor }> = {
|
|
admin: { label: 'Admin', color: 'sakura' },
|
|
user: { label: 'User', color: 'robin' },
|
|
moderator: {
|
|
label: 'Moderator',
|
|
color: 'sienna',
|
|
},
|
|
guest: { label: 'Guest', color: 'secondary' },
|
|
};
|
|
const roleInfo = roleMap[role];
|
|
return <Badge color={roleInfo.color}>{roleInfo.label}</Badge>;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'department',
|
|
header: 'Department',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>
|
|
{row.original.department}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'salary',
|
|
header: 'Salary',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const salary = parseFloat(row.getValue('salary') as string);
|
|
const formatted = new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(salary);
|
|
return (
|
|
<div style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857, color: '#008138' }}>
|
|
{formatted}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
],
|
|
data: users,
|
|
tableId: 'resize-demo-table',
|
|
enableColumnResizing: true,
|
|
enableSorting: false,
|
|
enableFiltering: false,
|
|
enableGlobalFilter: false,
|
|
enableColumnReordering: false,
|
|
enableColumnPinning: false,
|
|
enableRowSelection: false,
|
|
enablePagination: false,
|
|
showHeaders: true,
|
|
defaultColumnWidth: 200,
|
|
minColumnWidth: 100,
|
|
maxColumnWidth: 400,
|
|
},
|
|
};
|
|
|
|
// Story: All Features Demo
|
|
export const AllFeatures: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
All Features Demo
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
This table demonstrates all available features: column reordering, resizing, sorting,
|
|
filtering, pinning, row selection, and pagination. Try hovering over headers to see resize
|
|
handles, drag columns to reorder, click headers to sort, use the filter buttons, and
|
|
select rows with checkboxes.
|
|
</p>
|
|
<DataTable {...args} />
|
|
</div>
|
|
</div>
|
|
),
|
|
args: {
|
|
columns: [
|
|
{
|
|
id: 'serial',
|
|
header: '#',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 9999,
|
|
backgroundColor: 'var(--muted)',
|
|
color: 'var(--muted-foreground)',
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{row.index + 1}
|
|
</div>
|
|
),
|
|
size: 60,
|
|
},
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Employee Name',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div
|
|
style={{
|
|
backgroundImage: 'linear-gradient(to bottom right in oklab, #3080ff, #9810fa)',
|
|
height: 32,
|
|
width: 32,
|
|
borderRadius: 9999,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
lineHeight: 1.33333,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{row.original.name
|
|
.split(' ')
|
|
.map((n: string) => n[0])
|
|
.join('')}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>
|
|
{row.original.name}
|
|
</span>
|
|
<span style={{ fontSize: 12, lineHeight: 1.33333, color: 'var(--muted-foreground)' }}>
|
|
{row.original.email}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const role = row.original.role;
|
|
const roleMap: Record<User['role'], { label: string; color: BadgeColor }> = {
|
|
admin: {
|
|
label: 'Admin',
|
|
color: 'sakura',
|
|
},
|
|
user: {
|
|
label: 'User',
|
|
color: 'robin',
|
|
},
|
|
moderator: {
|
|
label: 'Moderator',
|
|
color: 'sienna',
|
|
},
|
|
guest: {
|
|
label: 'Guest',
|
|
color: 'secondary',
|
|
},
|
|
};
|
|
const roleInfo = roleMap[role];
|
|
return (
|
|
<Badge variant="outline" color={roleInfo.color}>
|
|
{roleInfo.label}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: 'Status',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const status = row.original.status;
|
|
const statusMap: Record<
|
|
User['status'],
|
|
{ label: string; icon: React.ComponentType; color: BadgeColor }
|
|
> = {
|
|
active: {
|
|
label: 'Active',
|
|
icon: CircleCheck,
|
|
color: 'success',
|
|
},
|
|
inactive: {
|
|
label: 'Inactive',
|
|
icon: CircleX,
|
|
color: 'error',
|
|
},
|
|
pending: {
|
|
label: 'Pending',
|
|
icon: Clock,
|
|
color: 'warning',
|
|
},
|
|
suspended: {
|
|
label: 'Suspended',
|
|
icon: CircleAlert,
|
|
color: 'secondary',
|
|
},
|
|
};
|
|
const statusInfo = statusMap[status];
|
|
const Icon = statusInfo.icon;
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<Icon />
|
|
<Badge variant="outline" color={statusInfo.color}>
|
|
{statusInfo.label}
|
|
</Badge>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'department',
|
|
header: 'Department',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>
|
|
{row.original.department}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'salary',
|
|
header: 'Salary',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const salary = parseFloat(row.getValue('salary') as string);
|
|
const formatted = new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(salary);
|
|
return (
|
|
<div style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857, color: '#008138' }}>
|
|
{formatted}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'performance',
|
|
header: 'Performance',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const performance = parseFloat(row.getValue('performance') as string);
|
|
const getPerformanceColor = (score: number) => {
|
|
if (score >= 90) return 'var(--accent-forest)';
|
|
if (score >= 80) return 'var(--accent-primary)';
|
|
if (score >= 70) return 'var(--accent-amber)';
|
|
return 'var(--accent-cherry)';
|
|
};
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<div style={{ flex: 1, backgroundColor: '#e5e7eb', borderRadius: 9999, height: 8 }}>
|
|
<div
|
|
style={{
|
|
height: 8,
|
|
borderRadius: 9999,
|
|
backgroundColor: getPerformanceColor(performance),
|
|
width: `${performance}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
<span
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
fontWeight: 500,
|
|
color: getPerformanceColor(performance),
|
|
}}
|
|
>
|
|
{performance}%
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'lastLogin',
|
|
header: 'Last Login',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const date = new Date(row.getValue('lastLogin') as string);
|
|
const formatted = date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
return (
|
|
<span style={{ fontSize: 14, lineHeight: 1.42857, color: 'var(--muted-foreground)' }}>
|
|
{formatted}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'actions',
|
|
header: 'Actions',
|
|
cell: () => {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Eye style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Pencil style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.Destructive}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Trash2 style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
],
|
|
data: users,
|
|
tableId: 'all-features-table',
|
|
enableSorting: true,
|
|
enableFiltering: true,
|
|
enableGlobalFilter: true,
|
|
enableColumnReordering: true,
|
|
enableColumnResizing: true,
|
|
enableColumnPinning: true,
|
|
enableRowSelection: true,
|
|
enablePagination: true,
|
|
pageSize: 5,
|
|
showHeaders: true,
|
|
defaultColumnWidth: 180,
|
|
minColumnWidth: 100,
|
|
maxColumnWidth: 400,
|
|
},
|
|
};
|
|
|
|
const fixedDate = 1771949360343; // 2026-02-24T16:09:20.343Z
|
|
|
|
// Generate large dataset for virtualization demo (supports offset for unique ids)
|
|
const generateLargeDataset = (count: number, startIndex = 0): User[] => {
|
|
const departments = [
|
|
'Engineering',
|
|
'Marketing',
|
|
'Sales',
|
|
'Support',
|
|
'Product',
|
|
'Design',
|
|
'Finance',
|
|
'HR',
|
|
];
|
|
const roles = ['admin', 'user', 'moderator', 'guest'] as const;
|
|
const statuses = ['active', 'inactive', 'pending', 'suspended'] as const;
|
|
|
|
return Array.from({ length: count }, (_, index) => ({
|
|
id: `${startIndex + index + 1}`,
|
|
name: `User ${startIndex + index + 1}`,
|
|
email: `user${startIndex + index + 1}@company.com`,
|
|
role: roles[index % roles.length],
|
|
status: statuses[index % statuses.length],
|
|
lastLogin: new Date(fixedDate + 60 * 1000 * index).toISOString(),
|
|
createdAt: new Date(fixedDate - 60 * 1000 * index).toISOString().split('T')[0],
|
|
avatar: `https://images.unsplash.com/photo-${1500000000000 + index}?w=32&h=32&fit=crop&crop=face`,
|
|
department: departments[index % departments.length],
|
|
salary: 50000 + 1000 * index,
|
|
performance: 60 + (index % 40),
|
|
}));
|
|
};
|
|
|
|
const largeDataset = generateLargeDataset(1000, 0);
|
|
|
|
// Story: Virtualization with All Features
|
|
export const VirtualizationWithFeatures: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
Virtualization with All Features
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
This table demonstrates virtualization with 1000 rows, plus all interactive features:
|
|
column reordering, resizing, sorting, filtering, and row selection. The table uses virtual
|
|
scrolling for optimal performance with large datasets. Try scrolling, resizing columns,
|
|
reordering, and selecting rows to see how virtualization maintains smooth performance.
|
|
</p>
|
|
<DataTable {...args} fixedHeight={600} />
|
|
</div>
|
|
</div>
|
|
),
|
|
args: {
|
|
columns: [
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Employee Name',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div
|
|
style={{
|
|
backgroundImage: 'linear-gradient(to bottom right in oklab, #3080ff, #9810fa)',
|
|
height: 32,
|
|
width: 32,
|
|
borderRadius: 9999,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
lineHeight: 1.33333,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{row.original.name
|
|
.split(' ')
|
|
.map((n: string) => n[0])
|
|
.join('')}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>
|
|
{row.original.name}
|
|
</span>
|
|
<span style={{ fontSize: 12, lineHeight: 1.33333, color: 'var(--muted-foreground)' }}>
|
|
{row.original.email}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const role = row.original.role;
|
|
const roleMap: Record<User['role'], { label: string; color: BadgeColor }> = {
|
|
admin: {
|
|
label: 'Admin',
|
|
color: 'sakura',
|
|
},
|
|
user: {
|
|
label: 'User',
|
|
color: 'robin',
|
|
},
|
|
moderator: {
|
|
label: 'Moderator',
|
|
color: 'sienna',
|
|
},
|
|
guest: {
|
|
label: 'Guest',
|
|
color: 'secondary',
|
|
},
|
|
};
|
|
const roleInfo = roleMap[role];
|
|
return (
|
|
<Badge variant="outline" color={roleInfo.color}>
|
|
{roleInfo.label}
|
|
</Badge>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: 'Status',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const status = row.original.status;
|
|
const statusMap: Record<
|
|
User['status'],
|
|
{ label: string; icon: React.ComponentType; color: BadgeColor }
|
|
> = {
|
|
active: {
|
|
label: 'Active',
|
|
icon: CircleCheck,
|
|
color: 'success',
|
|
},
|
|
inactive: {
|
|
label: 'Inactive',
|
|
icon: CircleX,
|
|
color: 'error',
|
|
},
|
|
pending: {
|
|
label: 'Pending',
|
|
icon: Clock,
|
|
color: 'warning',
|
|
},
|
|
suspended: {
|
|
label: 'Suspended',
|
|
icon: CircleAlert,
|
|
color: 'secondary',
|
|
},
|
|
};
|
|
const statusInfo = statusMap[status];
|
|
const Icon = statusInfo.icon;
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<Icon />
|
|
<Badge variant="outline" color={statusInfo.color}>
|
|
{statusInfo.label}
|
|
</Badge>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'department',
|
|
header: 'Department',
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>
|
|
{row.original.department}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'salary',
|
|
header: 'Salary',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const salary = parseFloat(row.getValue('salary') as string);
|
|
const formatted = new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(salary);
|
|
return (
|
|
<div style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857, color: '#008138' }}>
|
|
{formatted}
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'performance',
|
|
header: 'Performance',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const performance = parseFloat(row.getValue('performance') as string);
|
|
const getPerformanceColor = (score: number) => {
|
|
if (score >= 90) return 'var(--accent-forest)';
|
|
if (score >= 80) return 'var(--accent-primary)';
|
|
if (score >= 70) return 'var(--accent-amber)';
|
|
return 'var(--accent-cherry)';
|
|
};
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<div style={{ flex: 1, backgroundColor: '#e5e7eb', borderRadius: 9999, height: 8 }}>
|
|
<div
|
|
style={{
|
|
height: 8,
|
|
borderRadius: 9999,
|
|
backgroundColor: getPerformanceColor(performance),
|
|
width: `${performance}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
<span
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
fontWeight: 500,
|
|
color: getPerformanceColor(performance),
|
|
}}
|
|
>
|
|
{performance}%
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'lastLogin',
|
|
header: 'Last Login',
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const date = new Date(row.getValue('lastLogin') as string);
|
|
const formatted = date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
return (
|
|
<span style={{ fontSize: 14, lineHeight: 1.42857, color: 'var(--muted-foreground)' }}>
|
|
{formatted}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: 'actions',
|
|
header: 'Actions',
|
|
cell: () => {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Eye style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Pencil style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
color={ButtonColor.Destructive}
|
|
size="sm"
|
|
style={{ height: 32, width: 32, padding: 0 }}
|
|
>
|
|
<Trash2 style={{ height: 16, width: 16 }} />
|
|
</Button>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
],
|
|
data: largeDataset,
|
|
tableId: 'virtualization-features-table',
|
|
enableSorting: true,
|
|
enableFiltering: true,
|
|
enableGlobalFilter: true,
|
|
enableColumnReordering: true,
|
|
enableColumnResizing: true,
|
|
enableColumnPinning: true,
|
|
enableRowSelection: true,
|
|
enablePagination: false, // Disable pagination for virtualization demo
|
|
showHeaders: true,
|
|
defaultColumnWidth: 180,
|
|
minColumnWidth: 100,
|
|
maxColumnWidth: 400,
|
|
// Virtualization settings
|
|
enableVirtualization: true,
|
|
estimateRowSize: 60,
|
|
overscan: 10,
|
|
rowHeight: 60,
|
|
enableDynamicRowHeights: false,
|
|
},
|
|
};
|
|
|
|
// Story: Virtualized Infinite Scroll with Resize + Reorder
|
|
export const VirtualizedInfiniteScrollDndResize: StoryObj<typeof DataTable<User>> = {
|
|
render: (args) => {
|
|
const itemsPerPage = 100;
|
|
const maxItems = 1000;
|
|
|
|
// Initialize data synchronously to avoid Chromatic snapshot race conditions
|
|
const [data, setData] = React.useState<User[]>(() => generateLargeDataset(itemsPerPage, 0));
|
|
const [loading, setLoading] = React.useState(false);
|
|
const [hasMore, setHasMore] = React.useState(true);
|
|
const [page, setPage] = React.useState(1);
|
|
const [orderedColumns, setOrderedColumns] = React.useState<ColumnDef<User>[]>([]);
|
|
|
|
const loadMore = React.useCallback(() => {
|
|
if (loading || !hasMore) return;
|
|
setLoading(true);
|
|
// Simulate API latency
|
|
setTimeout(() => {
|
|
const offset = page * itemsPerPage;
|
|
const remaining = Math.max(0, maxItems - offset);
|
|
const take = Math.min(itemsPerPage, remaining);
|
|
const newData = take > 0 ? generateLargeDataset(take, offset) : [];
|
|
setData((prev) => [...prev, ...newData]);
|
|
const nextPage = page + 1;
|
|
setPage(nextPage);
|
|
setLoading(false);
|
|
if (nextPage * itemsPerPage >= maxItems) setHasMore(false);
|
|
}, 600);
|
|
}, [loading, hasMore, page]);
|
|
|
|
return (
|
|
<div className="stack-16">
|
|
<div
|
|
style={{
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
padding: 24,
|
|
backgroundColor: 'var(--background)',
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
fontSize: 18,
|
|
lineHeight: 1.55556,
|
|
fontWeight: 600,
|
|
marginBottom: 8,
|
|
color: 'var(--foreground)',
|
|
}}
|
|
>
|
|
Virtualized Infinite Scroll + Reorder + Resize
|
|
</h3>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
marginBottom: 16,
|
|
}}
|
|
>
|
|
Large dataset with virtualized rows, drag-and-drop column reordering, and on-change
|
|
column resizing. Scroll to load more.
|
|
</p>
|
|
<div
|
|
style={{
|
|
marginBottom: 16,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 16,
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--muted-foreground)',
|
|
}}
|
|
>
|
|
<span>Rows: {data.length}</span>
|
|
<span>Page: {page}</span>
|
|
{loading && <span>Loading…</span>}
|
|
{!hasMore && <span style={{ color: '#00a544' }}>All items loaded</span>}
|
|
{orderedColumns.length > 0 && (
|
|
<span style={{ textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden' }}>
|
|
Order:{' '}
|
|
{orderedColumns
|
|
.map((c) =>
|
|
String(
|
|
(c as { id?: string; accessorKey?: string }).id ??
|
|
(c as { accessorKey?: string }).accessorKey ??
|
|
'?'
|
|
)
|
|
)
|
|
.join(' | ')}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<DataTable
|
|
{...args}
|
|
data={data}
|
|
hasMore={hasMore}
|
|
onLoadMore={loadMore}
|
|
loadingMore={loading}
|
|
onColumnOrderChange={setOrderedColumns}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
args: {
|
|
columns: [
|
|
{
|
|
id: 'serial',
|
|
header: '#',
|
|
size: 72,
|
|
cell: ({ row }: { row: Row<User> }) => row.index + 1,
|
|
},
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Name',
|
|
size: 220,
|
|
minSize: 120,
|
|
maxSize: 360,
|
|
},
|
|
{
|
|
accessorKey: 'email',
|
|
header: 'Email',
|
|
size: 260,
|
|
minSize: 160,
|
|
maxSize: 460,
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
size: 140,
|
|
minSize: 100,
|
|
maxSize: 220,
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: 'Status',
|
|
size: 160,
|
|
minSize: 120,
|
|
maxSize: 240,
|
|
},
|
|
{
|
|
accessorKey: 'department',
|
|
header: 'Department',
|
|
size: 180,
|
|
minSize: 120,
|
|
maxSize: 280,
|
|
},
|
|
{
|
|
accessorKey: 'salary',
|
|
header: 'Salary',
|
|
size: 140,
|
|
minSize: 100,
|
|
maxSize: 220,
|
|
},
|
|
{
|
|
accessorKey: 'performance',
|
|
header: 'Performance',
|
|
size: 200,
|
|
minSize: 120,
|
|
maxSize: 300,
|
|
},
|
|
{
|
|
accessorKey: 'lastLogin',
|
|
header: 'Last Login',
|
|
size: 180,
|
|
minSize: 120,
|
|
maxSize: 260,
|
|
},
|
|
],
|
|
tableId: 'virtualized-infinite-reorder-resize',
|
|
enableSorting: false,
|
|
enableFiltering: true,
|
|
enableGlobalFilter: false,
|
|
enableColumnReordering: true,
|
|
enableColumnResizing: true,
|
|
enableColumnPinning: true,
|
|
enableRowSelection: true,
|
|
enablePagination: false,
|
|
showHeaders: true,
|
|
defaultColumnWidth: 180,
|
|
minColumnWidth: 80,
|
|
maxColumnWidth: 480,
|
|
// Virtualization + Infinite Scroll
|
|
enableVirtualization: true,
|
|
estimateRowSize: 56,
|
|
overscan: 10,
|
|
rowHeight: 56,
|
|
enableInfiniteScroll: true,
|
|
enableScrollRestoration: false,
|
|
fixedHeight: 600,
|
|
},
|
|
parameters: {
|
|
chromatic: {
|
|
// Disable animations to prevent visual diffs from spinning loaders
|
|
pauseAnimationAtEnd: true,
|
|
},
|
|
},
|
|
};
|
|
|
|
const meta: Meta<typeof DataTable<User>> = {
|
|
title: 'Composed Components/DataTable',
|
|
component: DataTable,
|
|
parameters: {
|
|
layout: 'fullscreen',
|
|
docs: {
|
|
description: {
|
|
component: `
|
|
## DataTable Component
|
|
|
|
The DataTable component is a powerful, feature-rich table built on top of TanStack Table. It provides advanced functionality including:
|
|
|
|
### Key Features
|
|
- **Sorting**: Click column headers to sort data
|
|
- **Filtering**: Filter data by individual columns or globally
|
|
- **Column Reordering**: Drag and drop columns to reorder them
|
|
- **Column Resizing**: Resize columns by dragging their edges
|
|
- **Column Pinning**: Pin columns to the left or right
|
|
- **Row Selection**: Select individual or all rows
|
|
- **Pagination**: Navigate through large datasets
|
|
- **Virtualization**: Efficient rendering of large datasets
|
|
- **Row Expansion**: Expand rows to show additional content
|
|
- **Custom Cells**: Fully customizable cell rendering
|
|
- **State Persistence**: Automatically saves user preferences
|
|
|
|
### Usage Examples
|
|
- **Basic**: Simple table with essential features
|
|
- **Advanced**: Full-featured table with all capabilities
|
|
- **Column Reordering**: Demo of drag-and-drop column reordering
|
|
- **Row Selection**: Interactive row selection with highlighting
|
|
- **Compact**: Space-efficient view for mobile or constrained layouts
|
|
|
|
### Props
|
|
The DataTable accepts all standard table features as props, allowing you to enable or disable specific functionality based on your needs.
|
|
`,
|
|
},
|
|
},
|
|
},
|
|
tags: ['autodocs'],
|
|
argTypes: {
|
|
enableSorting: {
|
|
control: 'boolean',
|
|
description: 'Enable column sorting functionality',
|
|
},
|
|
enableFiltering: {
|
|
control: 'boolean',
|
|
description: 'Enable individual column filtering',
|
|
},
|
|
enableGlobalFilter: {
|
|
control: 'boolean',
|
|
description: 'Enable global search across all columns',
|
|
},
|
|
enableColumnReordering: {
|
|
control: 'boolean',
|
|
description: 'Enable drag-and-drop column reordering',
|
|
},
|
|
enableColumnResizing: {
|
|
control: 'boolean',
|
|
description: 'Enable column width resizing',
|
|
},
|
|
enableColumnPinning: {
|
|
control: 'boolean',
|
|
description: 'Enable column pinning to left or right',
|
|
},
|
|
enableRowSelection: {
|
|
control: 'boolean',
|
|
description: 'Enable row selection with checkboxes',
|
|
},
|
|
enablePagination: {
|
|
control: 'boolean',
|
|
description: 'Enable pagination controls',
|
|
},
|
|
pageSize: {
|
|
control: 'number',
|
|
description: 'Number of rows per page',
|
|
},
|
|
showHeaders: {
|
|
control: 'boolean',
|
|
description: 'Show or hide table headers',
|
|
},
|
|
enableStickyHeaders: {
|
|
control: 'boolean',
|
|
description: 'Enable sticky headers that remain visible when scrolling',
|
|
},
|
|
fixedHeight: {
|
|
control: 'text',
|
|
description: 'Fixed height for the table container (e.g., "400px" or 400)',
|
|
},
|
|
},
|
|
};
|
|
|
|
export default meta;
|
|
|
|
export const StickyHeaders: StoryObj<typeof DataTable<User>> = {
|
|
args: {
|
|
columns: [
|
|
{
|
|
accessorKey: 'id',
|
|
header: 'ID',
|
|
size: 80,
|
|
},
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Employee Name',
|
|
size: 200,
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<div
|
|
style={{
|
|
backgroundImage: 'linear-gradient(to bottom right in oklab, #3080ff, #9810fa)',
|
|
height: 32,
|
|
width: 32,
|
|
borderRadius: 9999,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
color: '#fff',
|
|
fontSize: 12,
|
|
lineHeight: 1.33333,
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{row.original.name
|
|
.split(' ')
|
|
.map((n) => n[0])
|
|
.join('')
|
|
.toUpperCase()}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<span style={{ fontWeight: 500, fontSize: 14, lineHeight: 1.42857 }}>
|
|
{row.original.name}
|
|
</span>
|
|
<span style={{ fontSize: 12, lineHeight: 1.33333, color: 'var(--muted-foreground)' }}>
|
|
{row.original.email}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
size: 120,
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const roleMap: Record<User['role'], { label: string; color: BadgeColor }> = {
|
|
admin: { label: 'Admin', color: 'error' },
|
|
user: { label: 'User', color: 'robin' },
|
|
moderator: {
|
|
label: 'Moderator',
|
|
color: 'warning',
|
|
},
|
|
guest: { label: 'Guest', color: 'secondary' },
|
|
};
|
|
const role = roleMap[row.original.role];
|
|
return <Badge color={role.color}>{role.label}</Badge>;
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: 'Status',
|
|
size: 120,
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const statusMap: Record<User['status'], { icon: IconComponent; color: string }> = {
|
|
active: { icon: CircleCheck, color: 'var(--accent-forest)' },
|
|
inactive: { icon: CircleX, color: 'var(--accent-cherry)' },
|
|
pending: { icon: Clock, color: 'var(--accent-amber)' },
|
|
suspended: { icon: CircleAlert, color: 'var(--accent-sienna)' },
|
|
};
|
|
const status = statusMap[row.original.status];
|
|
const Icon = status.icon;
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<Icon style={{ height: 16, width: 16, color: status.color }} />
|
|
<span style={{ textTransform: 'capitalize', fontSize: 14, lineHeight: 1.42857 }}>
|
|
{row.original.status}
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'department',
|
|
header: 'Department',
|
|
size: 150,
|
|
},
|
|
{
|
|
accessorKey: 'salary',
|
|
header: 'Annual Salary',
|
|
size: 140,
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<span
|
|
style={{
|
|
fontFamily:
|
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
}}
|
|
>
|
|
${row.original.salary.toLocaleString()}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'performance',
|
|
header: 'Performance',
|
|
size: 120,
|
|
cell: ({ row }: { row: Row<User> }) => {
|
|
const score = row.original.performance;
|
|
const getPerformanceColor = (score: number) => {
|
|
if (score >= 90) return 'var(--accent-forest)';
|
|
if (score >= 80) return 'var(--accent-primary)';
|
|
if (score >= 70) return 'var(--accent-amber)';
|
|
return 'var(--accent-cherry)';
|
|
};
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
<span
|
|
style={{
|
|
fontWeight: 500,
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: getPerformanceColor(score),
|
|
}}
|
|
>
|
|
{score}%
|
|
</span>
|
|
<div
|
|
style={{
|
|
width: 80,
|
|
backgroundColor: '#e5e7eb',
|
|
borderRadius: 9999,
|
|
height: 8,
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
height: 8,
|
|
borderRadius: 9999,
|
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
backgroundColor: getPerformanceColor(score),
|
|
width: `${score}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: 'lastLogin',
|
|
header: 'Last Active',
|
|
size: 140,
|
|
cell: ({ row }: { row: Row<User> }) => (
|
|
<span style={{ fontSize: 14, lineHeight: 1.42857, color: 'var(--muted-foreground)' }}>
|
|
{new Date(row.original.lastLogin).toLocaleDateString()}
|
|
</span>
|
|
),
|
|
},
|
|
],
|
|
data: generateLargeDataset(100), // More rows to better demonstrate scrolling
|
|
tableId: 'sticky-headers-table',
|
|
enableSorting: true,
|
|
enableFiltering: true,
|
|
enableColumnResizing: true,
|
|
enableColumnReordering: true,
|
|
enableColumnPinning: true,
|
|
enableRowSelection: true,
|
|
enableStickyHeaders: true,
|
|
fixedHeight: 600, // Taller for better demo
|
|
showHeaders: true,
|
|
defaultColumnWidth: 150,
|
|
minColumnWidth: 80,
|
|
maxColumnWidth: 800, // Increased to allow full width usage
|
|
},
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story: `
|
|
## Sticky Headers
|
|
|
|
This example demonstrates the sticky headers feature with an employee directory. The table has a fixed height of 500px, and when you scroll through the data, the headers remain visible at the top.
|
|
|
|
### Key Features:
|
|
- **Sticky Headers**: Headers stay visible while scrolling through 100+ employee records
|
|
- **Fixed Height**: Table container has a defined height with smooth scrolling
|
|
- **Rich Data Display**: Shows employee avatars, performance bars, and status indicators
|
|
- **All Interactive Features**: Includes sorting, filtering, column resizing, and row selection
|
|
- **No Layout Shift**: Headers maintain perfect alignment during scroll
|
|
|
|
### Visual Improvements:
|
|
- **Employee Cards**: Name and email displayed together with avatar
|
|
- **Performance Bars**: Visual progress bars for performance scores
|
|
- **Status Indicators**: Icons with color-coded status
|
|
- **Better Typography**: Improved text hierarchy and spacing
|
|
|
|
### Usage:
|
|
\`\`\`tsx
|
|
<DataTable
|
|
columns={columns}
|
|
data={data}
|
|
tableId="sticky-table"
|
|
enableStickyHeaders={true}
|
|
fixedHeight={500}
|
|
enableSorting={true}
|
|
enableFiltering={true}
|
|
enableColumnResizing={true}
|
|
enableRowSelection={true}
|
|
/>
|
|
\`\`\`
|
|
`,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
// Story: Scroll to Index Functionality
|
|
export const ScrollToIndex: StoryObj<typeof DataTable<User>> = {
|
|
args: {
|
|
columns: enhancedColumns,
|
|
data: generateLargeDataset(200), // Generate more data for better scrolling demo
|
|
tableId: 'scroll-to-index-table',
|
|
enableVirtualization: true,
|
|
enableStickyHeaders: true,
|
|
fixedHeight: 500,
|
|
enableSorting: true,
|
|
enableFiltering: true,
|
|
enableColumnResizing: true,
|
|
enableRowSelection: true,
|
|
estimateRowSize: 60,
|
|
overscan: 5,
|
|
},
|
|
render: (args) => {
|
|
const ScrollToIndexDemo = () => {
|
|
const scrollToIndexRef = React.useRef<
|
|
((rowIndex: number, options?: { align?: 'start' | 'center' | 'end' }) => void) | undefined
|
|
>();
|
|
|
|
const handleScrollToUser = (userId: string) => {
|
|
const userIndex = args.data.findIndex((user) => user.id === userId);
|
|
console.log(`[Virtualized] Looking for user ID: ${userId}, found at index: ${userIndex}`);
|
|
console.log(
|
|
'[Virtualized] Available users:',
|
|
args.data.slice(0, 5).map((u) => ({ id: u.id, name: u.name }))
|
|
);
|
|
if (userIndex !== -1 && scrollToIndexRef.current) {
|
|
console.log(`[Virtualized] Scrolling to index: ${userIndex}`);
|
|
scrollToIndexRef.current(userIndex, { align: 'center' });
|
|
} else {
|
|
console.log(
|
|
`[Virtualized] User with ID ${userId} not found or scrollToIndexRef not available`
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleScrollToRandom = () => {
|
|
if (scrollToIndexRef.current) {
|
|
const randomIndex = Math.floor(Math.random() * args.data.length);
|
|
scrollToIndexRef.current(randomIndex, { align: 'center' });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="stack-16">
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
<Button
|
|
onClick={() => handleScrollToUser('1')}
|
|
variant="outlined"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
>
|
|
Scroll to User 1
|
|
</Button>
|
|
<Button
|
|
onClick={() => handleScrollToUser('50')}
|
|
variant="outlined"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
>
|
|
Scroll to User 50
|
|
</Button>
|
|
<Button
|
|
onClick={() => handleScrollToUser('100')}
|
|
variant="outlined"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
>
|
|
Scroll to User 100
|
|
</Button>
|
|
<Button
|
|
onClick={() => handleScrollToUser('150')}
|
|
variant="outlined"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
>
|
|
Scroll to User 150
|
|
</Button>
|
|
<Button
|
|
onClick={() => handleScrollToUser('200')}
|
|
variant="outlined"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
>
|
|
Scroll to Last User
|
|
</Button>
|
|
<Button
|
|
onClick={handleScrollToRandom}
|
|
variant="outlined"
|
|
color={ButtonColor.None}
|
|
size="sm"
|
|
>
|
|
Scroll to Random User
|
|
</Button>
|
|
</div>
|
|
<DataTable {...args} scrollToIndexRef={scrollToIndexRef} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return <ScrollToIndexDemo />;
|
|
},
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story: `
|
|
## Scroll to Index Functionality
|
|
|
|
This example demonstrates the new scroll to index functionality that allows you to programmatically scroll to specific rows in the table.
|
|
|
|
### Key Features:
|
|
- **Programmatic Scrolling**: Scroll to any row by its index
|
|
- **Alignment Options**: Choose how the row is positioned in the viewport
|
|
- **Virtualization Support**: Works seamlessly with virtualized tables
|
|
- **Interactive Controls**: Buttons to test different scroll scenarios
|
|
|
|
### Scroll Options:
|
|
- **\`align: 'start'\`**: Align row to the top of the viewport
|
|
- **\`align: 'center'\`**: Align row to the center of the viewport (default)
|
|
- **\`align: 'end'\`**: Align row to the bottom of the viewport
|
|
|
|
### Usage:
|
|
\`\`\`tsx
|
|
import { useRef } from 'react';
|
|
|
|
function MyTable({ users }) {
|
|
const scrollToIndexRef = useRef<((rowIndex: number, options?: { align?: 'start' | 'center' | 'end' }) => void) | undefined>();
|
|
|
|
const scrollToUser = (userId: string) => {
|
|
const userIndex = users.findIndex(user => user.id === userId);
|
|
if (userIndex !== -1 && scrollToIndexRef.current) {
|
|
scrollToIndexRef.current(userIndex, { align: 'center' });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<DataTable
|
|
columns={columns}
|
|
data={users}
|
|
tableId="my-table"
|
|
fixedHeight={500}
|
|
enableVirtualization={true}
|
|
scrollToIndexRef={scrollToIndexRef}
|
|
/>
|
|
);
|
|
}
|
|
\`\`\`
|
|
|
|
### Benefits:
|
|
- **User Experience**: Allow users to quickly navigate to specific data
|
|
- **Search Integration**: Perfect for highlighting search results
|
|
- **Navigation**: Build custom navigation controls
|
|
- **Performance**: Efficient scrolling even with large datasets
|
|
`,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
export const IndividualColumnWidths: StoryObj<typeof DataTable<User>> = {
|
|
args: {
|
|
columns: enhancedColumns, // Uses columns with individual size/minSize/maxSize
|
|
data: users,
|
|
tableId: 'individual-widths-table',
|
|
enableColumnResizing: true,
|
|
defaultColumnWidth: 150,
|
|
fixedHeight: 600,
|
|
minColumnWidth: 80,
|
|
maxColumnWidth: 400,
|
|
},
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story: `
|
|
## Individual Column Width Constraints
|
|
|
|
This example demonstrates how to set individual width constraints for each column using TanStack Table's built-in properties.
|
|
|
|
### Column Width Properties:
|
|
- **\`size\`**: Default width of the column
|
|
- **\`minSize\`**: Minimum width the column can be resized to
|
|
- **\`maxSize\`**: Maximum width the column can be resized to
|
|
|
|
### Example Configuration:
|
|
\`\`\`tsx
|
|
const columns: ColumnDef<User>[] = [
|
|
{
|
|
accessorKey: 'name',
|
|
header: 'Employee',
|
|
size: 250, // Default width
|
|
minSize: 200, // Minimum width
|
|
maxSize: 350, // Maximum width
|
|
cell: ({ row }) => { /* ... */ }
|
|
},
|
|
{
|
|
accessorKey: 'role',
|
|
header: 'Role',
|
|
size: 120, // Default width
|
|
minSize: 100, // Minimum width
|
|
maxSize: 150, // Maximum width
|
|
cell: ({ row }) => { /* ... */ }
|
|
},
|
|
// ... more columns
|
|
];
|
|
\`\`\`
|
|
|
|
### Benefits:
|
|
- **Precise Control**: Each column can have its own width constraints
|
|
- **Better UX**: Prevents columns from becoming too narrow or too wide
|
|
- **Responsive Design**: Columns maintain appropriate proportions
|
|
- **Override Globals**: Individual constraints override global minColumnWidth/maxColumnWidth
|
|
|
|
### Current Column Constraints:
|
|
- **Employee**: 200-350px (default: 250px)
|
|
- **Role**: 100-150px (default: 120px)
|
|
- **Status**: 120-180px (default: 140px)
|
|
- **Department**: 120-200px (default: 150px)
|
|
- **Salary**: 100-150px (default: 120px)
|
|
- **Performance**: 150-250px (default: 180px)
|
|
- **Last Login**: 120-180px (default: 140px)
|
|
`,
|
|
},
|
|
},
|
|
},
|
|
};
|