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