Files
zeekay 464e1eece0 State a version once
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.
2026-09-15 11:45:32 -07:00

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 &quot;Name&quot; 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)
`,
},
},
},
};