bolt-table 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1328 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
A high-performance, zero-dependency\* React table component. Only the rows visible in the viewport are ever in the DOM — making it fast for datasets of any size uisng [TanStack Virtual](https://tanstack.com/virtual).
|
|
4
4
|
|
|
5
|
-
Checkout the examples / demo at [bolt-table](https://bolt-table.vercel.app/)
|
|
6
|
-
Checkout the source code [here](https://github.com/venkateshwebdev/Bolt-Table)
|
|
7
|
-
|
|
8
5
|
[](https://www.npmjs.com/package/bolt-table)
|
|
9
6
|
[](./LICENSE)
|
|
7
|
+
[]
|
|
8
|
+
[]
|
|
9
|
+
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -413,6 +413,1331 @@ By default, BoltTable auto-sizes to its content. To fill a fixed-height containe
|
|
|
413
413
|
|
|
414
414
|
---
|
|
415
415
|
|
|
416
|
+
## Documentation
|
|
417
|
+
|
|
418
|
+
A complete guide to every feature in BoltTable. Each section explains the concept, shows the relevant types, and provides copy-paste code.
|
|
419
|
+
|
|
420
|
+
### Table of Contents
|
|
421
|
+
|
|
422
|
+
- [Core Concepts](#core-concepts)
|
|
423
|
+
- [Column Definitions](#column-definitions-in-depth)
|
|
424
|
+
- [Data & Row Keys](#data--row-keys)
|
|
425
|
+
- [Sorting](#sorting-in-depth)
|
|
426
|
+
- [Filtering](#filtering-in-depth)
|
|
427
|
+
- [Pagination](#pagination-in-depth)
|
|
428
|
+
- [Row Selection](#row-selection-in-depth)
|
|
429
|
+
- [Expandable Rows](#expandable-rows-in-depth)
|
|
430
|
+
- [Column Interactions](#column-interactions)
|
|
431
|
+
- [Loading States](#loading-states)
|
|
432
|
+
- [Infinite Scroll](#infinite-scroll-in-depth)
|
|
433
|
+
- [Empty States](#empty-states)
|
|
434
|
+
- [Styling & Theming](#styling--theming)
|
|
435
|
+
- [Context Menu](#context-menu)
|
|
436
|
+
- [Auto Height vs Fixed Height](#auto-height-vs-fixed-height)
|
|
437
|
+
- [Custom Icons](#custom-icons-1)
|
|
438
|
+
- [TypeScript](#typescript)
|
|
439
|
+
- [Server-Side Operations](#server-side-operations)
|
|
440
|
+
- [Performance](#performance)
|
|
441
|
+
- [Next.js & Frameworks](#nextjs--frameworks)
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
### Core Concepts
|
|
446
|
+
|
|
447
|
+
BoltTable is built around three ideas:
|
|
448
|
+
|
|
449
|
+
1. **Virtualization** — Only the rows visible in the viewport exist in the DOM. Scroll through 100,000 rows as smoothly as 10. Powered by [TanStack Virtual](https://tanstack.com/virtual).
|
|
450
|
+
|
|
451
|
+
2. **Client-side or server-side — your choice** — Every interactive feature (sorting, filtering, pagination) works in two modes. Omit the callback and BoltTable handles it locally. Provide the callback and BoltTable delegates to you.
|
|
452
|
+
|
|
453
|
+
3. **Zero configuration styling** — BoltTable renders with inline styles by default. No CSS imports, no Tailwind dependency, no CSS variables to set up. It works in light mode, dark mode, and everything in between. Then customize with `classNames` and `styles` when you need to.
|
|
454
|
+
|
|
455
|
+
**Minimum viable table:**
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
import { BoltTable, ColumnType } from 'bolt-table';
|
|
459
|
+
|
|
460
|
+
const columns: ColumnType<{ id: string; name: string }>[] = [
|
|
461
|
+
{ key: 'name', dataIndex: 'name', title: 'Name' },
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
<BoltTable columns={columns} data={[{ id: '1', name: 'Alice' }]} />
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
That gives you virtualization, column reordering (drag headers), column resizing (drag edges), a right-click context menu with sort/filter/pin/hide, and auto-height sizing — all with zero configuration.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### Column Definitions In-Depth
|
|
472
|
+
|
|
473
|
+
Columns are the backbone of BoltTable. Each column is an object conforming to `ColumnType<T>`.
|
|
474
|
+
|
|
475
|
+
#### Required fields
|
|
476
|
+
|
|
477
|
+
| Field | Type | Description |
|
|
478
|
+
|-------|------|-------------|
|
|
479
|
+
| `key` | `string` | Unique identifier. Used internally for drag-and-drop, pinning, hiding, sorting. |
|
|
480
|
+
| `dataIndex` | `string` | The property name on your row object to read. Must match a key on `T`. |
|
|
481
|
+
| `title` | `string \| ReactNode` | What appears in the header. Can be a string or any React element. |
|
|
482
|
+
|
|
483
|
+
#### Layout fields
|
|
484
|
+
|
|
485
|
+
| Field | Type | Default | Description |
|
|
486
|
+
|-------|------|---------|-------------|
|
|
487
|
+
| `width` | `number` | `150` | Column width in pixels. The **last visible column** always stretches to fill remaining space using CSS `minmax()`. |
|
|
488
|
+
| `hidden` | `boolean` | `false` | Controlled visibility. When `true`, the column is not rendered. |
|
|
489
|
+
| `defaultHidden` | `boolean` | `false` | Uncontrolled initial visibility. Column starts hidden but can be shown via context menu. |
|
|
490
|
+
| `pinned` | `'left' \| 'right' \| false` | `false` | Controlled pin state. Pinned columns stick to the edge during horizontal scroll. |
|
|
491
|
+
| `defaultPinned` | `'left' \| 'right' \| false` | `false` | Uncontrolled initial pin state. |
|
|
492
|
+
| `className` | `string` | — | CSS class applied to every cell in this column (header + body). |
|
|
493
|
+
| `style` | `CSSProperties` | — | Inline styles applied to every cell in this column. |
|
|
494
|
+
|
|
495
|
+
#### Behavior fields
|
|
496
|
+
|
|
497
|
+
| Field | Type | Default | Description |
|
|
498
|
+
|-------|------|---------|-------------|
|
|
499
|
+
| `sortable` | `boolean` | `true` | Whether sort controls appear for this column. |
|
|
500
|
+
| `sorter` | `boolean \| (a: T, b: T) => number` | — | `true` uses the default comparator. A function gives you full control. |
|
|
501
|
+
| `filterable` | `boolean` | `true` | Whether filter controls appear in the context menu. |
|
|
502
|
+
| `filterFn` | `(value: string, record: T, dataIndex: string) => boolean` | — | Custom filter predicate. Falls back to case-insensitive substring match. |
|
|
503
|
+
|
|
504
|
+
#### Custom rendering
|
|
505
|
+
|
|
506
|
+
| Field | Type | Description |
|
|
507
|
+
|-------|------|-------------|
|
|
508
|
+
| `render` | `(value: unknown, record: T, index: number) => ReactNode` | Custom cell renderer. If omitted, the raw value is rendered as text. |
|
|
509
|
+
| `shimmerRender` | `() => ReactNode` | Custom loading skeleton for this column. |
|
|
510
|
+
|
|
511
|
+
#### Example: a fully configured column
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
const nameColumn: ColumnType<User> = {
|
|
515
|
+
key: 'name',
|
|
516
|
+
dataIndex: 'name',
|
|
517
|
+
title: 'Full Name',
|
|
518
|
+
width: 220,
|
|
519
|
+
pinned: 'left',
|
|
520
|
+
sortable: true,
|
|
521
|
+
sorter: (a, b) => a.name.localeCompare(b.name),
|
|
522
|
+
filterable: true,
|
|
523
|
+
filterFn: (filterValue, record) =>
|
|
524
|
+
record.name.toLowerCase().includes(filterValue.toLowerCase()),
|
|
525
|
+
render: (value, record) => (
|
|
526
|
+
<div>
|
|
527
|
+
<strong>{record.name}</strong>
|
|
528
|
+
<span style={{ color: '#888', marginLeft: 8 }}>{record.email}</span>
|
|
529
|
+
</div>
|
|
530
|
+
),
|
|
531
|
+
shimmerRender: () => (
|
|
532
|
+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
533
|
+
<div style={{ width: 32, height: 32, borderRadius: '50%', background: '#eee' }} />
|
|
534
|
+
<div style={{ width: 120, height: 14, borderRadius: 4, background: '#eee' }} />
|
|
535
|
+
</div>
|
|
536
|
+
),
|
|
537
|
+
className: 'font-medium',
|
|
538
|
+
style: { minWidth: 180 },
|
|
539
|
+
};
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Data & Row Keys
|
|
545
|
+
|
|
546
|
+
#### The `data` prop
|
|
547
|
+
|
|
548
|
+
Pass an array of objects. Each object is one row. The shape must match your generic type `T`:
|
|
549
|
+
|
|
550
|
+
```tsx
|
|
551
|
+
interface Order {
|
|
552
|
+
[key: string]: unknown;
|
|
553
|
+
id: string;
|
|
554
|
+
customer: string;
|
|
555
|
+
total: number;
|
|
556
|
+
status: 'pending' | 'shipped' | 'delivered';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const orders: Order[] = [
|
|
560
|
+
{ id: 'ord-1', customer: 'Acme Corp', total: 1250, status: 'shipped' },
|
|
561
|
+
{ id: 'ord-2', customer: 'Globex', total: 3400, status: 'pending' },
|
|
562
|
+
];
|
|
563
|
+
|
|
564
|
+
<BoltTable<Order> columns={columns} data={orders} />
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
For **client-side** operations, pass the full dataset. BoltTable handles slicing, sorting, and filtering internally.
|
|
568
|
+
|
|
569
|
+
For **server-side** operations, pass only the current page. Handle sort/filter/paginate in your API layer.
|
|
570
|
+
|
|
571
|
+
#### The `rowKey` prop
|
|
572
|
+
|
|
573
|
+
BoltTable needs a unique identifier for each row — for selection, expansion, and stable virtualizer keys.
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
576
|
+
// String — reads record[rowKey]
|
|
577
|
+
<BoltTable rowKey="id" />
|
|
578
|
+
|
|
579
|
+
// Function — compute the key yourself
|
|
580
|
+
<BoltTable rowKey={(record) => `${record.type}-${record.id}`} />
|
|
581
|
+
|
|
582
|
+
// Default is "id" when omitted
|
|
583
|
+
<BoltTable />
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
The `rowKey` is always coerced to a string internally.
|
|
587
|
+
|
|
588
|
+
#### The `DataRecord` constraint
|
|
589
|
+
|
|
590
|
+
BoltTable's generic parameter requires `T extends DataRecord` where `DataRecord = Record<string, unknown>`. This means your interface needs an index signature:
|
|
591
|
+
|
|
592
|
+
```tsx
|
|
593
|
+
// This works
|
|
594
|
+
interface User {
|
|
595
|
+
[key: string]: unknown;
|
|
596
|
+
id: string;
|
|
597
|
+
name: string;
|
|
598
|
+
email: string;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// This also works
|
|
602
|
+
type User = {
|
|
603
|
+
id: string;
|
|
604
|
+
name: string;
|
|
605
|
+
email: string;
|
|
606
|
+
};
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
TypeScript `type` aliases satisfy `Record<string, unknown>` implicitly. If you use `interface`, add `[key: string]: unknown;` as the first line. This does not change the behavior of your type — all named properties retain their specific types.
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
### Sorting In-Depth
|
|
614
|
+
|
|
615
|
+
Sorting has two modes based on whether you provide the `onSortChange` callback.
|
|
616
|
+
|
|
617
|
+
#### Client-side sorting (default)
|
|
618
|
+
|
|
619
|
+
When `onSortChange` is **not** provided, BoltTable sorts the data array in memory.
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
const columns: ColumnType<User>[] = [
|
|
623
|
+
{
|
|
624
|
+
key: 'name',
|
|
625
|
+
dataIndex: 'name',
|
|
626
|
+
title: 'Name',
|
|
627
|
+
sortable: true,
|
|
628
|
+
// Option 1: default comparator (localeCompare for strings, subtraction for numbers)
|
|
629
|
+
sorter: true,
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
key: 'age',
|
|
633
|
+
dataIndex: 'age',
|
|
634
|
+
title: 'Age',
|
|
635
|
+
sortable: true,
|
|
636
|
+
// Option 2: custom comparator
|
|
637
|
+
sorter: (a, b) => a.age - b.age,
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
key: 'createdAt',
|
|
641
|
+
dataIndex: 'createdAt',
|
|
642
|
+
title: 'Created',
|
|
643
|
+
sortable: true,
|
|
644
|
+
// Option 3: complex custom logic
|
|
645
|
+
sorter: (a, b) =>
|
|
646
|
+
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
|
647
|
+
},
|
|
648
|
+
];
|
|
649
|
+
|
|
650
|
+
<BoltTable columns={columns} data={users} />
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Sort cycle**: click a column header (or use the context menu) to cycle through `null → 'asc' → 'desc' → null`. Only one column is sorted at a time.
|
|
654
|
+
|
|
655
|
+
#### Server-side sorting
|
|
656
|
+
|
|
657
|
+
When `onSortChange` **is** provided, BoltTable does **not** sort locally. It fires the callback and displays data as-is.
|
|
658
|
+
|
|
659
|
+
```tsx
|
|
660
|
+
const [sortKey, setSortKey] = useState('');
|
|
661
|
+
const [sortDir, setSortDir] = useState<SortDirection>(null);
|
|
662
|
+
|
|
663
|
+
<BoltTable
|
|
664
|
+
columns={columns}
|
|
665
|
+
data={serverData}
|
|
666
|
+
onSortChange={(columnKey, direction) => {
|
|
667
|
+
setSortKey(columnKey);
|
|
668
|
+
setSortDir(direction);
|
|
669
|
+
// direction is 'asc', 'desc', or null (sort cleared)
|
|
670
|
+
refetch({ sort: columnKey, order: direction });
|
|
671
|
+
}}
|
|
672
|
+
/>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
#### Disabling sort on specific columns
|
|
676
|
+
|
|
677
|
+
```tsx
|
|
678
|
+
{
|
|
679
|
+
key: 'actions',
|
|
680
|
+
dataIndex: 'id',
|
|
681
|
+
title: '',
|
|
682
|
+
sortable: false, // no sort controls for this column
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
### Filtering In-Depth
|
|
689
|
+
|
|
690
|
+
Filtering also has client-side and server-side modes.
|
|
691
|
+
|
|
692
|
+
#### Client-side filtering (default)
|
|
693
|
+
|
|
694
|
+
When `onFilterChange` is **not** provided, BoltTable filters in memory. Users filter via the right-click context menu on column headers.
|
|
695
|
+
|
|
696
|
+
```tsx
|
|
697
|
+
const columns: ColumnType<User>[] = [
|
|
698
|
+
{
|
|
699
|
+
key: 'status',
|
|
700
|
+
dataIndex: 'status',
|
|
701
|
+
title: 'Status',
|
|
702
|
+
filterable: true,
|
|
703
|
+
// Custom filter: exact match
|
|
704
|
+
filterFn: (filterValue, record) =>
|
|
705
|
+
record.status.toLowerCase() === filterValue.toLowerCase(),
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
key: 'name',
|
|
709
|
+
dataIndex: 'name',
|
|
710
|
+
title: 'Name',
|
|
711
|
+
filterable: true,
|
|
712
|
+
// No filterFn → falls back to case-insensitive substring match
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
key: 'tags',
|
|
716
|
+
dataIndex: 'tags',
|
|
717
|
+
title: 'Tags',
|
|
718
|
+
filterable: false, // disable filtering for this column
|
|
719
|
+
},
|
|
720
|
+
];
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
The default fallback filter converts the cell value to a string and checks if it includes the filter value (case-insensitive).
|
|
724
|
+
|
|
725
|
+
#### Server-side filtering
|
|
726
|
+
|
|
727
|
+
When `onFilterChange` **is** provided, BoltTable skips local filtering and passes the filters map to you:
|
|
728
|
+
|
|
729
|
+
```tsx
|
|
730
|
+
<BoltTable
|
|
731
|
+
columns={columns}
|
|
732
|
+
data={serverData}
|
|
733
|
+
onFilterChange={(filters) => {
|
|
734
|
+
// filters is Record<string, string>
|
|
735
|
+
// e.g. { status: "active", region: "us-east" }
|
|
736
|
+
// A column is removed from the map when its filter is cleared
|
|
737
|
+
setActiveFilters(filters);
|
|
738
|
+
refetch({ filters });
|
|
739
|
+
}}
|
|
740
|
+
/>
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
#### How users interact with filters
|
|
744
|
+
|
|
745
|
+
1. Right-click a column header
|
|
746
|
+
2. Click "Filter Column" in the context menu
|
|
747
|
+
3. Type a filter value in the input
|
|
748
|
+
4. Press Enter or click the checkmark to apply
|
|
749
|
+
5. To clear: right-click again and click "Clear Filter"
|
|
750
|
+
|
|
751
|
+
When a filter is active, a small filter icon appears in the column header.
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
### Pagination In-Depth
|
|
756
|
+
|
|
757
|
+
BoltTable renders a pagination footer with page navigation, page size selector, and a "showing X-Y of Z" label.
|
|
758
|
+
|
|
759
|
+
#### Client-side pagination
|
|
760
|
+
|
|
761
|
+
Pass all your data. BoltTable slices it per page:
|
|
762
|
+
|
|
763
|
+
```tsx
|
|
764
|
+
<BoltTable
|
|
765
|
+
columns={columns}
|
|
766
|
+
data={allUsers} // all 500 users
|
|
767
|
+
pagination={{ pageSize: 20 }}
|
|
768
|
+
/>
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
BoltTable manages the current page internally. If you want controlled page state:
|
|
772
|
+
|
|
773
|
+
```tsx
|
|
774
|
+
const [page, setPage] = useState(1);
|
|
775
|
+
const [pageSize, setPageSize] = useState(20);
|
|
776
|
+
|
|
777
|
+
<BoltTable
|
|
778
|
+
columns={columns}
|
|
779
|
+
data={allUsers}
|
|
780
|
+
pagination={{
|
|
781
|
+
current: page,
|
|
782
|
+
pageSize,
|
|
783
|
+
total: allUsers.length,
|
|
784
|
+
}}
|
|
785
|
+
onPaginationChange={(newPage, newSize) => {
|
|
786
|
+
setPage(newPage);
|
|
787
|
+
setPageSize(newSize);
|
|
788
|
+
}}
|
|
789
|
+
/>
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
#### Server-side pagination
|
|
793
|
+
|
|
794
|
+
Pass only the current page's data. Tell BoltTable the total so it can render page numbers:
|
|
795
|
+
|
|
796
|
+
```tsx
|
|
797
|
+
const [page, setPage] = useState(1);
|
|
798
|
+
const [pageSize, setPageSize] = useState(20);
|
|
799
|
+
const { data, total } = useFetchUsers({ page, pageSize });
|
|
800
|
+
|
|
801
|
+
<BoltTable
|
|
802
|
+
columns={columns}
|
|
803
|
+
data={data} // only current page from API
|
|
804
|
+
pagination={{
|
|
805
|
+
current: page,
|
|
806
|
+
pageSize,
|
|
807
|
+
total, // total across all pages
|
|
808
|
+
showTotal: (total, [from, to]) =>
|
|
809
|
+
`Showing ${from}–${to} of ${total} users`,
|
|
810
|
+
}}
|
|
811
|
+
onPaginationChange={(newPage, newSize) => {
|
|
812
|
+
setPage(newPage);
|
|
813
|
+
setPageSize(newSize);
|
|
814
|
+
}}
|
|
815
|
+
/>
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
#### `PaginationType` reference
|
|
819
|
+
|
|
820
|
+
| Field | Type | Default | Description |
|
|
821
|
+
|-------|------|---------|-------------|
|
|
822
|
+
| `current` | `number` | `1` | Active page number (1-based). |
|
|
823
|
+
| `pageSize` | `number` | `10` | Rows per page. Users can change this via the footer selector. |
|
|
824
|
+
| `total` | `number` | `data.length` | Total row count. Required for server-side pagination. |
|
|
825
|
+
| `showTotal` | `(total, [from, to]) => ReactNode` | — | Custom label for the "showing X of Y" text. |
|
|
826
|
+
|
|
827
|
+
#### Disabling pagination
|
|
828
|
+
|
|
829
|
+
```tsx
|
|
830
|
+
<BoltTable columns={columns} data={data} pagination={false} />
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
When pagination is `false`, all rows are rendered in a single scrollable viewport (virtualized).
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
### Row Selection In-Depth
|
|
838
|
+
|
|
839
|
+
Row selection prepends a checkbox (or radio) column to the left of the table.
|
|
840
|
+
|
|
841
|
+
#### Checkbox selection (multi-select)
|
|
842
|
+
|
|
843
|
+
```tsx
|
|
844
|
+
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
|
845
|
+
|
|
846
|
+
<BoltTable
|
|
847
|
+
columns={columns}
|
|
848
|
+
data={data}
|
|
849
|
+
rowKey="id"
|
|
850
|
+
rowSelection={{
|
|
851
|
+
type: 'checkbox',
|
|
852
|
+
selectedRowKeys: selectedKeys,
|
|
853
|
+
onChange: (keys, rows, info) => {
|
|
854
|
+
setSelectedKeys(keys);
|
|
855
|
+
// info.type is 'all' | 'single' | 'multiple'
|
|
856
|
+
},
|
|
857
|
+
}}
|
|
858
|
+
/>
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
The header checkbox toggles select-all. When some (but not all) rows are selected, it shows an indeterminate state.
|
|
862
|
+
|
|
863
|
+
#### Radio selection (single-select)
|
|
864
|
+
|
|
865
|
+
```tsx
|
|
866
|
+
<BoltTable
|
|
867
|
+
rowSelection={{
|
|
868
|
+
type: 'radio',
|
|
869
|
+
selectedRowKeys: selectedKeys,
|
|
870
|
+
onChange: (keys) => setSelectedKeys(keys),
|
|
871
|
+
}}
|
|
872
|
+
/>
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
Only one row can be selected at a time. The header checkbox is hidden automatically.
|
|
876
|
+
|
|
877
|
+
#### Disabling specific rows
|
|
878
|
+
|
|
879
|
+
```tsx
|
|
880
|
+
<BoltTable
|
|
881
|
+
rowSelection={{
|
|
882
|
+
type: 'checkbox',
|
|
883
|
+
selectedRowKeys: selectedKeys,
|
|
884
|
+
onChange: (keys) => setSelectedKeys(keys),
|
|
885
|
+
getCheckboxProps: (record) => ({
|
|
886
|
+
disabled: record.status === 'locked' || record.role === 'admin',
|
|
887
|
+
}),
|
|
888
|
+
}}
|
|
889
|
+
/>
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
Disabled rows render a grayed-out checkbox and cannot be toggled.
|
|
893
|
+
|
|
894
|
+
#### Hiding the select-all checkbox
|
|
895
|
+
|
|
896
|
+
```tsx
|
|
897
|
+
<BoltTable
|
|
898
|
+
rowSelection={{
|
|
899
|
+
type: 'checkbox',
|
|
900
|
+
hideSelectAll: true,
|
|
901
|
+
selectedRowKeys: selectedKeys,
|
|
902
|
+
onChange: (keys) => setSelectedKeys(keys),
|
|
903
|
+
}}
|
|
904
|
+
/>
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
#### `RowSelectionConfig<T>` reference
|
|
908
|
+
|
|
909
|
+
| Field | Type | Default | Description |
|
|
910
|
+
|-------|------|---------|-------------|
|
|
911
|
+
| `type` | `'checkbox' \| 'radio'` | `'checkbox'` | Selection control type. |
|
|
912
|
+
| `selectedRowKeys` | `React.Key[]` | — | Currently selected keys (controlled, required). |
|
|
913
|
+
| `onChange` | `(keys, rows, info) => void` | — | Called on any selection change. Primary callback. |
|
|
914
|
+
| `onSelect` | `(record, selected, rows, event) => void` | — | Called when a single row is toggled. |
|
|
915
|
+
| `onSelectAll` | `(selected, selectedRows, changeRows) => void` | — | Called when the header checkbox is toggled. |
|
|
916
|
+
| `getCheckboxProps` | `(record) => { disabled?: boolean }` | — | Per-row checkbox/radio props. |
|
|
917
|
+
| `hideSelectAll` | `boolean` | `false` | Hide the header select-all checkbox. |
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
### Expandable Rows In-Depth
|
|
922
|
+
|
|
923
|
+
Expandable rows reveal a content panel below each row when the user clicks the expand toggle.
|
|
924
|
+
|
|
925
|
+
#### Basic usage (uncontrolled)
|
|
926
|
+
|
|
927
|
+
```tsx
|
|
928
|
+
<BoltTable
|
|
929
|
+
columns={columns}
|
|
930
|
+
data={data}
|
|
931
|
+
rowKey="id"
|
|
932
|
+
expandable={{
|
|
933
|
+
rowExpandable: (record) => record.details != null,
|
|
934
|
+
expandedRowRender: (record) => (
|
|
935
|
+
<div style={{ padding: 16 }}>
|
|
936
|
+
<h4>{record.name}</h4>
|
|
937
|
+
<p>{record.description}</p>
|
|
938
|
+
</div>
|
|
939
|
+
),
|
|
940
|
+
}}
|
|
941
|
+
expandedRowHeight={200}
|
|
942
|
+
/>
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
BoltTable manages which rows are expanded internally.
|
|
946
|
+
|
|
947
|
+
#### Controlled expansion
|
|
948
|
+
|
|
949
|
+
```tsx
|
|
950
|
+
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
|
951
|
+
|
|
952
|
+
<BoltTable
|
|
953
|
+
expandable={{
|
|
954
|
+
expandedRowKeys: expandedKeys,
|
|
955
|
+
onExpandedRowsChange: (keys) => setExpandedKeys(keys),
|
|
956
|
+
expandedRowRender: (record) => <DetailPanel record={record} />,
|
|
957
|
+
}}
|
|
958
|
+
/>
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
When you provide `expandedRowKeys`, BoltTable operates in controlled mode. You must update the keys yourself in `onExpandedRowsChange`.
|
|
962
|
+
|
|
963
|
+
#### Default expanded rows
|
|
964
|
+
|
|
965
|
+
```tsx
|
|
966
|
+
<BoltTable
|
|
967
|
+
expandable={{
|
|
968
|
+
defaultExpandAllRows: true, // expand everything on mount
|
|
969
|
+
expandedRowRender: (record) => <Detail record={record} />,
|
|
970
|
+
}}
|
|
971
|
+
/>
|
|
972
|
+
|
|
973
|
+
// Or expand specific rows:
|
|
974
|
+
<BoltTable
|
|
975
|
+
expandable={{
|
|
976
|
+
defaultExpandedRowKeys: ['row-1', 'row-3'],
|
|
977
|
+
expandedRowRender: (record) => <Detail record={record} />,
|
|
978
|
+
}}
|
|
979
|
+
/>
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
#### Limiting expanded height
|
|
983
|
+
|
|
984
|
+
```tsx
|
|
985
|
+
<BoltTable
|
|
986
|
+
expandedRowHeight={200} // initial estimate for virtualizer
|
|
987
|
+
maxExpandedRowHeight={400} // panel becomes scrollable beyond this
|
|
988
|
+
expandable={{
|
|
989
|
+
expandedRowRender: (record) => <LongContent record={record} />,
|
|
990
|
+
}}
|
|
991
|
+
/>
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
The expanded panel auto-measures its content using `ResizeObserver`. The `expandedRowHeight` is only an estimate to prevent layout jumps — the real height takes over once measured.
|
|
995
|
+
|
|
996
|
+
#### `ExpandableConfig<T>` reference
|
|
997
|
+
|
|
998
|
+
| Field | Type | Default | Description |
|
|
999
|
+
|-------|------|---------|-------------|
|
|
1000
|
+
| `expandedRowRender` | `(record, index, indent, expanded) => ReactNode` | — | Renders the expanded content panel (required). |
|
|
1001
|
+
| `rowExpandable` | `(record) => boolean` | — | Controls which rows show the expand toggle. |
|
|
1002
|
+
| `expandedRowKeys` | `React.Key[]` | — | Controlled expanded keys. |
|
|
1003
|
+
| `defaultExpandedRowKeys` | `React.Key[]` | — | Initially expanded keys (uncontrolled). |
|
|
1004
|
+
| `defaultExpandAllRows` | `boolean` | `false` | Expand all rows on mount (uncontrolled). |
|
|
1005
|
+
| `onExpandedRowsChange` | `(keys) => void` | — | Called when expanded keys change. |
|
|
1006
|
+
| `showExpandIcon` | `(record) => boolean` | — | Controls expand icon visibility per row. |
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
|
|
1010
|
+
### Column Interactions
|
|
1011
|
+
|
|
1012
|
+
BoltTable supports four column interactions out of the box. All are enabled by default.
|
|
1013
|
+
|
|
1014
|
+
#### Column reordering (drag-and-drop)
|
|
1015
|
+
|
|
1016
|
+
Drag a column header to reorder. BoltTable uses a custom zero-dependency drag implementation — no `@dnd-kit` or other library needed.
|
|
1017
|
+
|
|
1018
|
+
```tsx
|
|
1019
|
+
<BoltTable
|
|
1020
|
+
columns={columns}
|
|
1021
|
+
data={data}
|
|
1022
|
+
onColumnOrderChange={(newOrder) => {
|
|
1023
|
+
// newOrder is string[] of column keys in their new positions
|
|
1024
|
+
console.log('New order:', newOrder);
|
|
1025
|
+
saveColumnOrder(newOrder);
|
|
1026
|
+
}}
|
|
1027
|
+
/>
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
Pinned columns cannot be dragged.
|
|
1031
|
+
|
|
1032
|
+
#### Column resizing
|
|
1033
|
+
|
|
1034
|
+
Drag the right edge of any header to resize. A colored overlay line and width label follow the cursor.
|
|
1035
|
+
|
|
1036
|
+
```tsx
|
|
1037
|
+
<BoltTable
|
|
1038
|
+
columns={columns}
|
|
1039
|
+
data={data}
|
|
1040
|
+
onColumnResize={(columnKey, newWidth) => {
|
|
1041
|
+
// newWidth is the final width in pixels after mouse-up
|
|
1042
|
+
saveColumnWidth(columnKey, newWidth);
|
|
1043
|
+
}}
|
|
1044
|
+
/>
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
#### Column pinning
|
|
1048
|
+
|
|
1049
|
+
Pin columns to the left or right edge so they stay visible during horizontal scroll.
|
|
1050
|
+
|
|
1051
|
+
**Declarative (in column definitions):**
|
|
1052
|
+
|
|
1053
|
+
```tsx
|
|
1054
|
+
const columns: ColumnType<User>[] = [
|
|
1055
|
+
{ key: 'name', dataIndex: 'name', title: 'Name', pinned: 'left' },
|
|
1056
|
+
{ key: 'email', dataIndex: 'email', title: 'Email' },
|
|
1057
|
+
{ key: 'actions', dataIndex: 'id', title: '', pinned: 'right' },
|
|
1058
|
+
];
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
**Runtime (via context menu):** Users right-click a header and select "Pin to Left" / "Pin to Right" / "Unpin".
|
|
1062
|
+
|
|
1063
|
+
```tsx
|
|
1064
|
+
<BoltTable
|
|
1065
|
+
columns={columns}
|
|
1066
|
+
data={data}
|
|
1067
|
+
onColumnPin={(columnKey, pinned) => {
|
|
1068
|
+
// pinned is 'left' | 'right' | false
|
|
1069
|
+
updateColumnConfig(columnKey, { pinned });
|
|
1070
|
+
}}
|
|
1071
|
+
/>
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
Pinned columns use `position: sticky` with a semi-transparent background.
|
|
1075
|
+
|
|
1076
|
+
#### Column hiding
|
|
1077
|
+
|
|
1078
|
+
Users can hide columns via the right-click context menu ("Hide Column"). Pinned columns cannot be hidden.
|
|
1079
|
+
|
|
1080
|
+
```tsx
|
|
1081
|
+
<BoltTable
|
|
1082
|
+
columns={columns}
|
|
1083
|
+
data={data}
|
|
1084
|
+
onColumnHide={(columnKey, hidden) => {
|
|
1085
|
+
// hidden is true (just hidden) or false (just shown)
|
|
1086
|
+
updateColumnConfig(columnKey, { hidden });
|
|
1087
|
+
}}
|
|
1088
|
+
/>
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
To programmatically control visibility:
|
|
1092
|
+
|
|
1093
|
+
```tsx
|
|
1094
|
+
const columns = allColumns.map(col => ({
|
|
1095
|
+
...col,
|
|
1096
|
+
hidden: hiddenSet.has(col.key),
|
|
1097
|
+
}));
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
### Loading States
|
|
1103
|
+
|
|
1104
|
+
BoltTable has two loading modes for different scenarios.
|
|
1105
|
+
|
|
1106
|
+
#### `isLoading` — shimmer rows in the body
|
|
1107
|
+
|
|
1108
|
+
When `data` is empty and `isLoading` is `true`, the entire body renders animated skeleton rows. Headers remain real.
|
|
1109
|
+
|
|
1110
|
+
When `data` is non-empty and `isLoading` is `true` (e.g., loading the next page in infinite scroll), skeleton rows are appended at the bottom below real data.
|
|
1111
|
+
|
|
1112
|
+
```tsx
|
|
1113
|
+
const [loading, setLoading] = useState(true);
|
|
1114
|
+
const [data, setData] = useState<User[]>([]);
|
|
1115
|
+
|
|
1116
|
+
useEffect(() => {
|
|
1117
|
+
fetchUsers().then((users) => {
|
|
1118
|
+
setData(users);
|
|
1119
|
+
setLoading(false);
|
|
1120
|
+
});
|
|
1121
|
+
}, []);
|
|
1122
|
+
|
|
1123
|
+
<BoltTable
|
|
1124
|
+
columns={columns}
|
|
1125
|
+
data={data}
|
|
1126
|
+
isLoading={loading}
|
|
1127
|
+
/>
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
#### `layoutLoading` — full skeleton (headers + body)
|
|
1131
|
+
|
|
1132
|
+
When you don't yet know column widths (e.g., columns are fetched from an API), use `layoutLoading` to show a complete skeleton:
|
|
1133
|
+
|
|
1134
|
+
```tsx
|
|
1135
|
+
<BoltTable
|
|
1136
|
+
columns={columns}
|
|
1137
|
+
data={data}
|
|
1138
|
+
layoutLoading={!columnsResolved}
|
|
1139
|
+
/>
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
The difference:
|
|
1143
|
+
|
|
1144
|
+
| State | Headers | Body |
|
|
1145
|
+
|-------|---------|------|
|
|
1146
|
+
| `isLoading=true`, data empty | Real headers | Shimmer rows |
|
|
1147
|
+
| `isLoading=true`, data present | Real headers | Real rows + shimmer rows at bottom |
|
|
1148
|
+
| `layoutLoading=true` | Shimmer headers | Shimmer rows |
|
|
1149
|
+
|
|
1150
|
+
#### Custom shimmer per column
|
|
1151
|
+
|
|
1152
|
+
```tsx
|
|
1153
|
+
{
|
|
1154
|
+
key: 'avatar',
|
|
1155
|
+
dataIndex: 'avatar',
|
|
1156
|
+
title: 'Avatar',
|
|
1157
|
+
width: 60,
|
|
1158
|
+
shimmerRender: () => (
|
|
1159
|
+
<div style={{
|
|
1160
|
+
width: 36, height: 36,
|
|
1161
|
+
borderRadius: '50%',
|
|
1162
|
+
background: 'linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%)',
|
|
1163
|
+
backgroundSize: '200% 100%',
|
|
1164
|
+
animation: 'shimmer 1.5s infinite',
|
|
1165
|
+
}} />
|
|
1166
|
+
),
|
|
1167
|
+
}
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
---
|
|
1171
|
+
|
|
1172
|
+
### Infinite Scroll In-Depth
|
|
1173
|
+
|
|
1174
|
+
Infinite scroll loads more rows as the user scrolls to the bottom.
|
|
1175
|
+
|
|
1176
|
+
```tsx
|
|
1177
|
+
const [data, setData] = useState<User[]>([]);
|
|
1178
|
+
const [loading, setLoading] = useState(false);
|
|
1179
|
+
const [hasMore, setHasMore] = useState(true);
|
|
1180
|
+
|
|
1181
|
+
const loadMore = useCallback(async () => {
|
|
1182
|
+
if (loading || !hasMore) return;
|
|
1183
|
+
setLoading(true);
|
|
1184
|
+
const { rows, total } = await fetchPage(data.length);
|
|
1185
|
+
setData(prev => [...prev, ...rows]);
|
|
1186
|
+
setHasMore(data.length + rows.length < total);
|
|
1187
|
+
setLoading(false);
|
|
1188
|
+
}, [loading, hasMore, data.length]);
|
|
1189
|
+
|
|
1190
|
+
<BoltTable
|
|
1191
|
+
columns={columns}
|
|
1192
|
+
data={data}
|
|
1193
|
+
rowKey="id"
|
|
1194
|
+
isLoading={loading}
|
|
1195
|
+
onEndReached={loadMore}
|
|
1196
|
+
onEndReachedThreshold={8} // trigger 8 rows from bottom
|
|
1197
|
+
pagination={false} // disable pagination for infinite scroll
|
|
1198
|
+
autoHeight={false} // fill parent container height
|
|
1199
|
+
/>
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
**Key points:**
|
|
1203
|
+
|
|
1204
|
+
- `onEndReached` fires when the last visible row is within `onEndReachedThreshold` rows of the end.
|
|
1205
|
+
- A built-in debounce guard prevents it from firing repeatedly. It resets when `data.length` changes or `isLoading` flips to `false`.
|
|
1206
|
+
- Set `pagination={false}` — pagination and infinite scroll are mutually exclusive patterns.
|
|
1207
|
+
- Set `autoHeight={false}` so the table fills a fixed-height container (otherwise it auto-sizes and there's nothing to scroll).
|
|
1208
|
+
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
### Empty States
|
|
1212
|
+
|
|
1213
|
+
When `data` is empty and `isLoading` is `false`, BoltTable renders an empty state.
|
|
1214
|
+
|
|
1215
|
+
#### Default
|
|
1216
|
+
|
|
1217
|
+
Without configuration, a simple "No data" message appears.
|
|
1218
|
+
|
|
1219
|
+
#### Custom empty renderer
|
|
1220
|
+
|
|
1221
|
+
```tsx
|
|
1222
|
+
<BoltTable
|
|
1223
|
+
columns={columns}
|
|
1224
|
+
data={[]}
|
|
1225
|
+
emptyRenderer={
|
|
1226
|
+
<div style={{
|
|
1227
|
+
display: 'flex',
|
|
1228
|
+
flexDirection: 'column',
|
|
1229
|
+
alignItems: 'center',
|
|
1230
|
+
gap: 12,
|
|
1231
|
+
padding: '48px 0',
|
|
1232
|
+
color: '#888',
|
|
1233
|
+
}}>
|
|
1234
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none"
|
|
1235
|
+
stroke="currentColor" strokeWidth="1.5">
|
|
1236
|
+
<circle cx="11" cy="11" r="8" />
|
|
1237
|
+
<path d="m21 21-4.3-4.3" />
|
|
1238
|
+
</svg>
|
|
1239
|
+
<p style={{ fontWeight: 500 }}>No results found</p>
|
|
1240
|
+
<p style={{ fontSize: 12 }}>Try adjusting your search or filters.</p>
|
|
1241
|
+
</div>
|
|
1242
|
+
}
|
|
1243
|
+
/>
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
The empty state is centered within the visible viewport — it stays centered even if the table is wider than the screen.
|
|
1247
|
+
|
|
1248
|
+
---
|
|
1249
|
+
|
|
1250
|
+
### Styling & Theming
|
|
1251
|
+
|
|
1252
|
+
BoltTable provides three layers of customization, from broad to granular.
|
|
1253
|
+
|
|
1254
|
+
#### Layer 1: `accentColor`
|
|
1255
|
+
|
|
1256
|
+
A single color string that themes all interactive elements:
|
|
1257
|
+
|
|
1258
|
+
```tsx
|
|
1259
|
+
<BoltTable
|
|
1260
|
+
accentColor="#6366f1" // indigo — applied to sort icons, filter icons,
|
|
1261
|
+
// resize line, selected rows, expand chevrons,
|
|
1262
|
+
// checkboxes, pagination highlight
|
|
1263
|
+
/>
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
Default is `#1890ff`.
|
|
1267
|
+
|
|
1268
|
+
#### Layer 2: `classNames`
|
|
1269
|
+
|
|
1270
|
+
CSS class overrides per table region. These are appended to (not replacing) the defaults:
|
|
1271
|
+
|
|
1272
|
+
```tsx
|
|
1273
|
+
<BoltTable
|
|
1274
|
+
classNames={{
|
|
1275
|
+
header: 'text-xs uppercase tracking-wider text-gray-500',
|
|
1276
|
+
cell: 'text-sm text-gray-900',
|
|
1277
|
+
row: 'border-b',
|
|
1278
|
+
pinnedHeader: 'border-r border-blue-200 bg-blue-50',
|
|
1279
|
+
pinnedCell: 'border-r border-blue-100',
|
|
1280
|
+
dragHeader: 'opacity-80 shadow-lg',
|
|
1281
|
+
expandedRow: 'bg-gray-50',
|
|
1282
|
+
}}
|
|
1283
|
+
/>
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
| Key | Applies to |
|
|
1287
|
+
|-----|-----------|
|
|
1288
|
+
| `header` | All non-pinned column header cells. |
|
|
1289
|
+
| `cell` | All body cells (pinned and non-pinned). |
|
|
1290
|
+
| `row` | Each row wrapper element. |
|
|
1291
|
+
| `pinnedHeader` | Pinned column headers (in addition to `header`). |
|
|
1292
|
+
| `pinnedCell` | Pinned column body cells (in addition to `cell`). |
|
|
1293
|
+
| `dragHeader` | The floating ghost column shown while dragging. |
|
|
1294
|
+
| `expandedRow` | The expanded content panel below each row. |
|
|
1295
|
+
|
|
1296
|
+
#### Layer 3: `styles`
|
|
1297
|
+
|
|
1298
|
+
Inline CSS overrides with the highest specificity:
|
|
1299
|
+
|
|
1300
|
+
```tsx
|
|
1301
|
+
<BoltTable
|
|
1302
|
+
styles={{
|
|
1303
|
+
header: { fontSize: 12, fontWeight: 600, letterSpacing: '0.05em' },
|
|
1304
|
+
cell: { fontSize: 14 },
|
|
1305
|
+
pinnedHeader: { borderRight: '2px solid #dbeafe' },
|
|
1306
|
+
pinnedCell: { borderRight: '1px solid #eff6ff' },
|
|
1307
|
+
pinnedBg: 'rgba(239, 246, 255, 0.95)',
|
|
1308
|
+
rowHover: { backgroundColor: '#f8fafc' },
|
|
1309
|
+
rowSelected: { backgroundColor: '#eff6ff' },
|
|
1310
|
+
dragHeader: { boxShadow: '0 4px 12px rgba(0,0,0,0.15)' },
|
|
1311
|
+
expandedRow: { borderTop: '1px solid #e2e8f0' },
|
|
1312
|
+
}}
|
|
1313
|
+
/>
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
| Key | Type | Description |
|
|
1317
|
+
|-----|------|-------------|
|
|
1318
|
+
| `header` | `CSSProperties` | Non-pinned header cells. |
|
|
1319
|
+
| `cell` | `CSSProperties` | Body cells. |
|
|
1320
|
+
| `row` | `CSSProperties` | Row wrapper. |
|
|
1321
|
+
| `pinnedHeader` | `CSSProperties` | Pinned headers (on top of `header`). |
|
|
1322
|
+
| `pinnedCell` | `CSSProperties` | Pinned body cells (on top of `cell`). |
|
|
1323
|
+
| `pinnedBg` | `string` | **CSS color** for pinned column backgrounds. |
|
|
1324
|
+
| `rowHover` | `CSSProperties` | Applied when a row is hovered. |
|
|
1325
|
+
| `rowSelected` | `CSSProperties` | Applied when a row is selected. |
|
|
1326
|
+
| `dragHeader` | `CSSProperties` | The ghost column while dragging. |
|
|
1327
|
+
| `expandedRow` | `CSSProperties` | Expanded content panel. |
|
|
1328
|
+
|
|
1329
|
+
#### Per-column styling
|
|
1330
|
+
|
|
1331
|
+
Each column can also have its own `className` and `style`:
|
|
1332
|
+
|
|
1333
|
+
```tsx
|
|
1334
|
+
{
|
|
1335
|
+
key: 'amount',
|
|
1336
|
+
dataIndex: 'amount',
|
|
1337
|
+
title: 'Amount',
|
|
1338
|
+
className: 'text-right font-mono',
|
|
1339
|
+
style: { fontVariantNumeric: 'tabular-nums' },
|
|
1340
|
+
}
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
---
|
|
1344
|
+
|
|
1345
|
+
### Context Menu
|
|
1346
|
+
|
|
1347
|
+
Right-clicking any column header opens a context menu with built-in actions:
|
|
1348
|
+
|
|
1349
|
+
1. **Sort Ascending** / **Sort Descending** — if `sortable` is not `false`
|
|
1350
|
+
2. **Filter Column** / **Clear Filter** — if `filterable` is not `false`
|
|
1351
|
+
3. **Pin to Left** / **Pin to Right** / **Unpin** — always available
|
|
1352
|
+
4. **Hide Column** — not available for pinned columns
|
|
1353
|
+
|
|
1354
|
+
#### Custom context menu items
|
|
1355
|
+
|
|
1356
|
+
Append your own items below the built-in ones:
|
|
1357
|
+
|
|
1358
|
+
```tsx
|
|
1359
|
+
import type { ColumnContextMenuItem } from 'bolt-table';
|
|
1360
|
+
|
|
1361
|
+
const customMenuItems: ColumnContextMenuItem[] = [
|
|
1362
|
+
{
|
|
1363
|
+
key: 'copy',
|
|
1364
|
+
label: 'Copy Column Data',
|
|
1365
|
+
icon: <CopyIcon className="h-3 w-3" />,
|
|
1366
|
+
onClick: (columnKey) => {
|
|
1367
|
+
const values = data.map(row => row[columnKey]);
|
|
1368
|
+
navigator.clipboard.writeText(values.join('\n'));
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
key: 'export',
|
|
1373
|
+
label: 'Export as CSV',
|
|
1374
|
+
onClick: (columnKey) => exportColumnAsCSV(columnKey),
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
key: 'delete',
|
|
1378
|
+
label: 'Remove Column',
|
|
1379
|
+
danger: true, // renders in red
|
|
1380
|
+
disabled: false, // can be dynamically disabled
|
|
1381
|
+
onClick: (columnKey) => removeColumn(columnKey),
|
|
1382
|
+
},
|
|
1383
|
+
];
|
|
1384
|
+
|
|
1385
|
+
<BoltTable
|
|
1386
|
+
columns={columns}
|
|
1387
|
+
data={data}
|
|
1388
|
+
columnContextMenuItems={customMenuItems}
|
|
1389
|
+
/>
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
#### `ColumnContextMenuItem` reference
|
|
1393
|
+
|
|
1394
|
+
| Field | Type | Description |
|
|
1395
|
+
|-------|------|-------------|
|
|
1396
|
+
| `key` | `string` | Unique identifier (used as React key). |
|
|
1397
|
+
| `label` | `ReactNode` | Menu item text. |
|
|
1398
|
+
| `icon` | `ReactNode` | Optional icon (12–14px recommended). |
|
|
1399
|
+
| `danger` | `boolean` | Renders label in red. |
|
|
1400
|
+
| `disabled` | `boolean` | Grays out the item; click handler not called. |
|
|
1401
|
+
| `onClick` | `(columnKey: string) => void` | Called with the column's key when clicked. |
|
|
1402
|
+
|
|
1403
|
+
---
|
|
1404
|
+
|
|
1405
|
+
### Auto Height vs Fixed Height
|
|
1406
|
+
|
|
1407
|
+
#### `autoHeight={true}` (default)
|
|
1408
|
+
|
|
1409
|
+
The table auto-sizes to its content, capped at 10 rows. Fewer rows = smaller table. More rows = capped at `10 × rowHeight`, with the remaining rows scrollable.
|
|
1410
|
+
|
|
1411
|
+
```tsx
|
|
1412
|
+
<BoltTable columns={columns} data={data} autoHeight={true} />
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
Use this when the table is part of a page layout and you want it to take only the space it needs.
|
|
1416
|
+
|
|
1417
|
+
#### `autoHeight={false}`
|
|
1418
|
+
|
|
1419
|
+
The table fills its parent container (`height: 100%`). The parent must provide a height.
|
|
1420
|
+
|
|
1421
|
+
```tsx
|
|
1422
|
+
<div style={{ height: 600 }}>
|
|
1423
|
+
<BoltTable columns={columns} data={data} autoHeight={false} />
|
|
1424
|
+
</div>
|
|
1425
|
+
|
|
1426
|
+
{/* Or with CSS */}
|
|
1427
|
+
<div className="h-[calc(100vh-200px)]">
|
|
1428
|
+
<BoltTable columns={columns} data={data} autoHeight={false} />
|
|
1429
|
+
</div>
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
Use this for:
|
|
1433
|
+
- Dashboard panels with fixed dimensions
|
|
1434
|
+
- Infinite scroll (the table needs a fixed viewport to scroll within)
|
|
1435
|
+
- Full-screen table views
|
|
1436
|
+
|
|
1437
|
+
---
|
|
1438
|
+
|
|
1439
|
+
### Custom Icons
|
|
1440
|
+
|
|
1441
|
+
Every built-in icon can be replaced via the `icons` prop. All default icons are inline SVGs at 12×12px.
|
|
1442
|
+
|
|
1443
|
+
```tsx
|
|
1444
|
+
import type { BoltTableIcons } from 'bolt-table';
|
|
1445
|
+
import {
|
|
1446
|
+
GripVertical, ArrowUpAZ, ArrowDownAZ, Filter, FilterX,
|
|
1447
|
+
Pin, PinOff, EyeOff, ChevronDown, ChevronLeft, ChevronRight,
|
|
1448
|
+
ChevronsLeft, ChevronsRight,
|
|
1449
|
+
} from 'lucide-react';
|
|
1450
|
+
|
|
1451
|
+
const icons: BoltTableIcons = {
|
|
1452
|
+
gripVertical: <GripVertical size={12} />,
|
|
1453
|
+
sortAsc: <ArrowUpAZ size={12} />,
|
|
1454
|
+
sortDesc: <ArrowDownAZ size={12} />,
|
|
1455
|
+
filter: <Filter size={12} />,
|
|
1456
|
+
filterClear: <FilterX size={12} />,
|
|
1457
|
+
pin: <Pin size={12} />,
|
|
1458
|
+
pinOff: <PinOff size={12} />,
|
|
1459
|
+
eyeOff: <EyeOff size={12} />,
|
|
1460
|
+
chevronDown: <ChevronDown size={12} />,
|
|
1461
|
+
chevronLeft: <ChevronLeft size={12} />,
|
|
1462
|
+
chevronRight: <ChevronRight size={12} />,
|
|
1463
|
+
chevronsLeft: <ChevronsLeft size={12} />,
|
|
1464
|
+
chevronsRight: <ChevronsRight size={12} />,
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
<BoltTable columns={columns} data={data} icons={icons} />
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
| Icon Key | Used In |
|
|
1471
|
+
|----------|---------|
|
|
1472
|
+
| `gripVertical` | Column header drag handle |
|
|
1473
|
+
| `sortAsc` | Sort ascending indicator in header |
|
|
1474
|
+
| `sortDesc` | Sort descending indicator in header |
|
|
1475
|
+
| `filter` | Filter active indicator in header |
|
|
1476
|
+
| `filterClear` | Clear filter button in context menu |
|
|
1477
|
+
| `pin` | Pin option in context menu |
|
|
1478
|
+
| `pinOff` | Unpin button on pinned headers |
|
|
1479
|
+
| `eyeOff` | Hide column option in context menu |
|
|
1480
|
+
| `chevronDown` | Expand row toggle / page size dropdown |
|
|
1481
|
+
| `chevronLeft` | Pagination: previous page |
|
|
1482
|
+
| `chevronRight` | Pagination: next page |
|
|
1483
|
+
| `chevronsLeft` | Pagination: first page |
|
|
1484
|
+
| `chevronsRight` | Pagination: last page |
|
|
1485
|
+
|
|
1486
|
+
To hide the grip icon entirely:
|
|
1487
|
+
|
|
1488
|
+
```tsx
|
|
1489
|
+
<BoltTable hideGripIcon={true} />
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
---
|
|
1493
|
+
|
|
1494
|
+
### TypeScript
|
|
1495
|
+
|
|
1496
|
+
BoltTable is fully typed. The main generic parameter is the row data type.
|
|
1497
|
+
|
|
1498
|
+
#### Generic usage
|
|
1499
|
+
|
|
1500
|
+
```tsx
|
|
1501
|
+
import { BoltTable, ColumnType, SortDirection, DataRecord } from 'bolt-table';
|
|
1502
|
+
|
|
1503
|
+
interface Product {
|
|
1504
|
+
[key: string]: unknown;
|
|
1505
|
+
id: string;
|
|
1506
|
+
name: string;
|
|
1507
|
+
price: number;
|
|
1508
|
+
category: string;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const columns: ColumnType<Product>[] = [
|
|
1512
|
+
{
|
|
1513
|
+
key: 'name',
|
|
1514
|
+
dataIndex: 'name',
|
|
1515
|
+
title: 'Product',
|
|
1516
|
+
render: (value, record) => {
|
|
1517
|
+
// `record` is typed as Product
|
|
1518
|
+
// `record.price` is `number`, not `unknown`
|
|
1519
|
+
return <span>{record.name} (${record.price})</span>;
|
|
1520
|
+
},
|
|
1521
|
+
sorter: (a, b) => {
|
|
1522
|
+
// `a` and `b` are typed as Product
|
|
1523
|
+
return a.name.localeCompare(b.name);
|
|
1524
|
+
},
|
|
1525
|
+
filterFn: (filterValue, record) => {
|
|
1526
|
+
// `record` is typed as Product
|
|
1527
|
+
return record.name.toLowerCase().includes(filterValue.toLowerCase());
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
];
|
|
1531
|
+
|
|
1532
|
+
<BoltTable<Product> columns={columns} data={products} />
|
|
1533
|
+
```
|
|
1534
|
+
|
|
1535
|
+
#### All exported types
|
|
1536
|
+
|
|
1537
|
+
```ts
|
|
1538
|
+
import type {
|
|
1539
|
+
BoltTableIcons, // Icon override map
|
|
1540
|
+
ColumnType, // Column definition
|
|
1541
|
+
ColumnContextMenuItem, // Custom context menu item
|
|
1542
|
+
DataRecord, // Base row type (Record<string, unknown>)
|
|
1543
|
+
ExpandableConfig, // Expandable rows configuration
|
|
1544
|
+
PaginationType, // Pagination configuration
|
|
1545
|
+
RowSelectionConfig, // Row selection configuration
|
|
1546
|
+
SortDirection, // 'asc' | 'desc' | null
|
|
1547
|
+
} from 'bolt-table';
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
#### `interface` vs `type` for row data
|
|
1551
|
+
|
|
1552
|
+
TypeScript `interface` declarations don't implicitly satisfy index signatures. If you use `interface`, add `[key: string]: unknown`:
|
|
1553
|
+
|
|
1554
|
+
```tsx
|
|
1555
|
+
// Works (type alias)
|
|
1556
|
+
type User = { id: string; name: string };
|
|
1557
|
+
|
|
1558
|
+
// Works (interface with index signature)
|
|
1559
|
+
interface User {
|
|
1560
|
+
[key: string]: unknown;
|
|
1561
|
+
id: string;
|
|
1562
|
+
name: string;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Does NOT work (interface without index signature)
|
|
1566
|
+
interface User {
|
|
1567
|
+
id: string;
|
|
1568
|
+
name: string;
|
|
1569
|
+
}
|
|
1570
|
+
// Error: Type 'User' does not satisfy the constraint 'DataRecord'.
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
---
|
|
1574
|
+
|
|
1575
|
+
### Server-Side Operations
|
|
1576
|
+
|
|
1577
|
+
A complete example of a table where sorting, filtering, and pagination are all handled by your API.
|
|
1578
|
+
|
|
1579
|
+
```tsx
|
|
1580
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
1581
|
+
import { BoltTable, ColumnType, SortDirection } from 'bolt-table';
|
|
1582
|
+
|
|
1583
|
+
interface User {
|
|
1584
|
+
[key: string]: unknown;
|
|
1585
|
+
id: string;
|
|
1586
|
+
name: string;
|
|
1587
|
+
email: string;
|
|
1588
|
+
role: string;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const columns: ColumnType<User>[] = [
|
|
1592
|
+
{ key: 'name', dataIndex: 'name', title: 'Name', width: 200, sortable: true, filterable: true },
|
|
1593
|
+
{ key: 'email', dataIndex: 'email', title: 'Email', width: 280, sortable: true, filterable: true },
|
|
1594
|
+
{ key: 'role', dataIndex: 'role', title: 'Role', width: 120, sortable: true, filterable: true },
|
|
1595
|
+
];
|
|
1596
|
+
|
|
1597
|
+
export default function UsersTable() {
|
|
1598
|
+
const [data, setData] = useState<User[]>([]);
|
|
1599
|
+
const [total, setTotal] = useState(0);
|
|
1600
|
+
const [loading, setLoading] = useState(true);
|
|
1601
|
+
const [page, setPage] = useState(1);
|
|
1602
|
+
const [pageSize, setPageSize] = useState(20);
|
|
1603
|
+
const [sortKey, setSortKey] = useState('');
|
|
1604
|
+
const [sortDir, setSortDir] = useState<SortDirection>(null);
|
|
1605
|
+
const [filters, setFilters] = useState<Record<string, string>>({});
|
|
1606
|
+
|
|
1607
|
+
const fetchData = useCallback(async () => {
|
|
1608
|
+
setLoading(true);
|
|
1609
|
+
const res = await fetch('/api/users?' + new URLSearchParams({
|
|
1610
|
+
page: String(page),
|
|
1611
|
+
pageSize: String(pageSize),
|
|
1612
|
+
...(sortKey && sortDir ? { sortKey, sortDir } : {}),
|
|
1613
|
+
...filters,
|
|
1614
|
+
}));
|
|
1615
|
+
const json = await res.json();
|
|
1616
|
+
setData(json.rows);
|
|
1617
|
+
setTotal(json.total);
|
|
1618
|
+
setLoading(false);
|
|
1619
|
+
}, [page, pageSize, sortKey, sortDir, filters]);
|
|
1620
|
+
|
|
1621
|
+
useEffect(() => { fetchData(); }, [fetchData]);
|
|
1622
|
+
|
|
1623
|
+
return (
|
|
1624
|
+
<BoltTable<User>
|
|
1625
|
+
columns={columns}
|
|
1626
|
+
data={data}
|
|
1627
|
+
rowKey="id"
|
|
1628
|
+
isLoading={loading}
|
|
1629
|
+
pagination={{ current: page, pageSize, total }}
|
|
1630
|
+
onPaginationChange={(p, s) => {
|
|
1631
|
+
setPage(p);
|
|
1632
|
+
setPageSize(s);
|
|
1633
|
+
}}
|
|
1634
|
+
onSortChange={(key, dir) => {
|
|
1635
|
+
setSortKey(key);
|
|
1636
|
+
setSortDir(dir);
|
|
1637
|
+
setPage(1); // reset to page 1 on sort change
|
|
1638
|
+
}}
|
|
1639
|
+
onFilterChange={(f) => {
|
|
1640
|
+
setFilters(f);
|
|
1641
|
+
setPage(1); // reset to page 1 on filter change
|
|
1642
|
+
}}
|
|
1643
|
+
/>
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
**The rule is simple**: provide the callback → BoltTable delegates. Omit the callback → BoltTable handles it locally.
|
|
1649
|
+
|
|
1650
|
+
| Feature | Client-side (local) | Server-side (delegated) |
|
|
1651
|
+
|---------|-------------------|----------------------|
|
|
1652
|
+
| Sorting | Omit `onSortChange` | Provide `onSortChange` |
|
|
1653
|
+
| Filtering | Omit `onFilterChange` | Provide `onFilterChange` |
|
|
1654
|
+
| Pagination | Pass all data, use `pagination={{ pageSize }}` | Pass current page, use `pagination={{ current, pageSize, total }}` + `onPaginationChange` |
|
|
1655
|
+
|
|
1656
|
+
---
|
|
1657
|
+
|
|
1658
|
+
### Performance
|
|
1659
|
+
|
|
1660
|
+
BoltTable is designed to be fast by default. Here are the key performance characteristics and tips.
|
|
1661
|
+
|
|
1662
|
+
#### Virtualization
|
|
1663
|
+
|
|
1664
|
+
Only the rows visible in the viewport (plus a small overscan buffer) exist in the DOM. Whether your dataset has 50 rows or 50,000, the DOM node count stays constant. This is powered by TanStack Virtual.
|
|
1665
|
+
|
|
1666
|
+
#### Memoize columns and data
|
|
1667
|
+
|
|
1668
|
+
BoltTable watches the `columns` array for changes using a content fingerprint. To avoid unnecessary re-renders, memoize your column definitions:
|
|
1669
|
+
|
|
1670
|
+
```tsx
|
|
1671
|
+
// Good — columns are computed once
|
|
1672
|
+
const columns = useMemo(() => buildColumns(), []);
|
|
1673
|
+
|
|
1674
|
+
// Good — data is memoized unless the source changes
|
|
1675
|
+
const data = useMemo(() => allData.slice(0, 50), [allData]);
|
|
1676
|
+
|
|
1677
|
+
// Bad — creates a new array on every render
|
|
1678
|
+
const columns = buildColumns();
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1681
|
+
#### Stable render functions
|
|
1682
|
+
|
|
1683
|
+
Column `render` functions should be stable references when possible. If your render function uses external state, wrap the column definition in `useMemo` with the relevant dependencies:
|
|
1684
|
+
|
|
1685
|
+
```tsx
|
|
1686
|
+
const columns = useMemo(() => [
|
|
1687
|
+
{
|
|
1688
|
+
key: 'name',
|
|
1689
|
+
dataIndex: 'name',
|
|
1690
|
+
title: 'Name',
|
|
1691
|
+
render: (value: unknown, record: User) => (
|
|
1692
|
+
<NameCell user={record} highlight={searchTerm} />
|
|
1693
|
+
),
|
|
1694
|
+
},
|
|
1695
|
+
], [searchTerm]);
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
#### Large datasets
|
|
1699
|
+
|
|
1700
|
+
For 10,000+ rows:
|
|
1701
|
+
- Set `pagination={false}` and `autoHeight={false}` for a fixed-height virtualized viewport
|
|
1702
|
+
- Avoid complex render functions that create many DOM nodes per cell
|
|
1703
|
+
- Use `rowHeight` to give all rows a uniform height (avoids dynamic measurement)
|
|
1704
|
+
|
|
1705
|
+
---
|
|
1706
|
+
|
|
1707
|
+
### Next.js & Frameworks
|
|
1708
|
+
|
|
1709
|
+
#### Next.js (App Router)
|
|
1710
|
+
|
|
1711
|
+
BoltTable uses browser APIs (`ResizeObserver`, DOM events, `window.matchMedia`) and must be a client component:
|
|
1712
|
+
|
|
1713
|
+
```tsx
|
|
1714
|
+
'use client';
|
|
1715
|
+
|
|
1716
|
+
import { BoltTable } from 'bolt-table';
|
|
1717
|
+
|
|
1718
|
+
export function UsersTable({ users }: { users: User[] }) {
|
|
1719
|
+
return <BoltTable columns={columns} data={users} rowKey="id" />;
|
|
1720
|
+
}
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
#### Next.js (Pages Router)
|
|
1724
|
+
|
|
1725
|
+
No special configuration needed. Pages Router components are client-side by default.
|
|
1726
|
+
|
|
1727
|
+
#### Remix / React Router
|
|
1728
|
+
|
|
1729
|
+
No special configuration needed. Works out of the box.
|
|
1730
|
+
|
|
1731
|
+
#### Vite
|
|
1732
|
+
|
|
1733
|
+
No special configuration needed. Works out of the box.
|
|
1734
|
+
|
|
1735
|
+
```tsx
|
|
1736
|
+
import { BoltTable } from 'bolt-table';
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
---
|
|
1740
|
+
|
|
416
1741
|
## Type exports
|
|
417
1742
|
|
|
418
1743
|
```ts
|
package/package.json
CHANGED