bolt-table 0.1.5 → 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.
Files changed (2) hide show
  1. package/README.md +1328 -2
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,10 +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
-
7
5
  [![npm version](https://img.shields.io/npm/v/bolt-table)](https://www.npmjs.com/package/bolt-table)
8
6
  [![license](https://img.shields.io/npm/l/bolt-table)](./LICENSE)
7
+ [![github](https://github.com/venkateshwebdev/Bolt-Table)]
8
+ [![Website](https://bolt-table.vercel.app/)]
9
+
9
10
 
10
11
  ---
11
12
 
@@ -412,6 +413,1331 @@ By default, BoltTable auto-sizes to its content. To fill a fixed-height containe
412
413
 
413
414
  ---
414
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
+
415
1741
  ## Type exports
416
1742
 
417
1743
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bolt-table",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Virtualized React table with column drag & drop, pinning, resizing, sorting, filtering, and pagination.",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",