bolt-table 0.1.14 → 0.1.15

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