bolt-table 0.1.26 → 0.1.28

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
@@ -1,6 +1,785 @@
1
1
  # bolt-table
2
2
 
3
- A high-performance, zero-dependency\* React table component. Only the rows visible in the viewport are ever in the DOM — making it fast for datasets of any size uisng [TanStack Virtual](https://tanstack.com/virtual).
3
+ A high-performance, zero-dependency\* React table component. Only the rows visible in the viewport are ever in the DOM — making it fast for datasets of any size using [TanStack Virtual](https://tanstack.com/virtual).
4
+
5
+ [![npm version](https://img.shields.io/npm/v/bolt-table)](https://www.npmjs.com/package/bolt-table)
6
+ [![license](https://img.shields.io/npm/l/bolt-table)](./LICENSE)
7
+ [![github](https://img.shields.io/badge/GitHub-Source-181717?logo=github)](https://github.com/venkateshwebdev/Bolt-Table)
8
+ [![website](https://img.shields.io/badge/Website-Live_Demo-blue?logo=vercel)](https://bolt-table.vercel.app/)
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ - **Row virtualization** — only visible rows are rendered, powered by TanStack Virtual
15
+ - **Horizontal virtualization** — optionally render only visible columns (great for 100+ column tables)
16
+ - **Dynamic row heights** — auto-measure row heights from content instead of using a fixed `rowHeight`
17
+ - **Drag to reorder columns** — custom zero-dependency drag-and-drop (no @dnd-kit needed)
18
+ - **Column pinning** — pin columns to the left or right edge via right-click
19
+ - **Column resizing** — drag the right edge of any header to resize
20
+ - **Column hiding** — hide/show columns via the right-click context menu
21
+ - **Column picker** — built-in checklist panel to toggle multiple columns on/off at once
22
+ - **Column persistence** — optional localStorage save/restore of column order, widths, visibility, and pinned state
23
+ - **Global search** — a search bar above the table that filters all rows across every column
24
+ - **Sorting** — client-side or server-side, with custom comparators per column
25
+ - **Filtering** — client-side or server-side, with custom filter functions per column
26
+ - **Pagination** — client-side slice or server-side with full control
27
+ - **Row selection** — checkbox or radio, with select-all, indeterminate state, and disabled rows
28
+ - **Expandable rows** — auto-measured content panels below each row, controlled or uncontrolled
29
+ - **Shimmer loading** — animated skeleton rows on initial load and infinite scroll append
30
+ - **Infinite scroll** — `onEndReached` callback with configurable threshold
31
+ - **Empty state** — custom renderer or default "No data" message
32
+ - **Auto height** — table shrinks/grows to fit rows, capped at 10 rows by default
33
+ - **Row pinning** — pin rows to the top or bottom of the table, sticky during vertical scroll
34
+ - **Cell context menu** — right-click (or long-press on mobile) any cell to pin rows or copy values
35
+ - **Right-click context menu** — sort, filter, pin, hide, plus custom items
36
+ - **Mobile-friendly context menus** — long-press (touch-and-hold) triggers context menus on touch devices
37
+ - **Nested / grouped columns** — group related columns under a shared header spanning multiple columns
38
+ - **Duplicate key safety** — automatically deduplicates row keys when data contains rows with the same ID
39
+ - **Theme-agnostic** — works in light and dark mode out of the box, no CSS variables needed
40
+ - **Editable cells** — right-click any cell on an `editable` column to inline-edit via the context menu
41
+ - **Custom icons** — override any built-in icon via the `icons` prop
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ npm install bolt-table @tanstack/react-virtual
49
+ ```
50
+
51
+ That's it. No other peer dependencies.
52
+
53
+ ---
54
+
55
+ ## Quick start
56
+
57
+ ```tsx
58
+ import { BoltTable, ColumnType } from 'bolt-table';
59
+
60
+ interface User {
61
+ id: string;
62
+ name: string;
63
+ email: string;
64
+ age: number;
65
+ }
66
+
67
+ const columns: ColumnType<User>[] = [
68
+ { key: 'name', dataIndex: 'name', title: 'Name', width: 200 },
69
+ { key: 'email', dataIndex: 'email', title: 'Email', width: 280 },
70
+ { key: 'age', dataIndex: 'age', title: 'Age', width: 80 },
71
+ ];
72
+
73
+ const data: User[] = [
74
+ { id: '1', name: 'Alice', email: 'alice@example.com', age: 28 },
75
+ { id: '2', name: 'Bob', email: 'bob@example.com', age: 34 },
76
+ { id: '3', name: 'Charlie', email: 'charlie@example.com', age: 22 },
77
+ ];
78
+
79
+ export default function App() {
80
+ return (
81
+ <BoltTable<User>
82
+ columns={columns}
83
+ data={data}
84
+ rowKey="id"
85
+ />
86
+ );
87
+ }
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Next.js (App Router)
93
+
94
+ BoltTable uses browser APIs and must be wrapped in a client boundary:
95
+
96
+ ```tsx
97
+ 'use client';
98
+ import { BoltTable } from 'bolt-table';
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Styling
104
+
105
+ BoltTable uses **inline CSS styles** for all defaults — no Tailwind, no CSS variables, no external stylesheets required. It works out of the box in any React project, light or dark mode.
106
+
107
+ You can customize everything via the `styles` and `classNames` props, including headers, cells, rows, pinned regions, and pagination. If your project uses Tailwind, you can pass Tailwind classes through `classNames` and they'll be applied on top of the inline defaults.
108
+
109
+ ### Custom icons
110
+
111
+ All built-in icons are inline SVGs. Override any icon via the `icons` prop:
112
+
113
+ ```tsx
114
+ import type { BoltTableIcons } from 'bolt-table';
115
+
116
+ <BoltTable
117
+ icons={{
118
+ gripVertical: <MyGripIcon size={12} />,
119
+ sortAsc: <MySortUpIcon size={12} />,
120
+ chevronsLeft: <MyFirstPageIcon size={12} />,
121
+ search: <MySearchIcon size={14} />,
122
+ columns: <MyColumnsIcon size={14} />,
123
+ close: <MyXIcon size={12} />,
124
+ }}
125
+ />
126
+ ```
127
+
128
+ Available icon keys: `gripVertical`, `sortAsc`, `sortDesc`, `filter`, `filterClear`, `pin`, `pinOff`, `eyeOff`, `chevronDown`, `chevronLeft`, `chevronRight`, `chevronsLeft`, `chevronsRight`, `copy`, `edit`, `search`, `columns`, `close`.
129
+
130
+ ---
131
+
132
+ ## Props
133
+
134
+ ### `BoltTable`
135
+
136
+ | Prop | Type | Default | Description |
137
+ |------|------|---------|-------------|
138
+ | `columns` | `ColumnType<T>[]` | — | Column definitions (required) |
139
+ | `data` | `T[]` | — | Row data array (required) |
140
+ | `rowKey` | `string \| (record: T) => string` | `'id'` | Unique row identifier. Duplicate keys are handled automatically |
141
+ | `rowHeight` | `number` | `40` | Base height of each row in pixels |
142
+ | `expandedRowHeight` | `number` | `200` | Estimated height for expanded rows |
143
+ | `maxExpandedRowHeight` | `number` | — | Max height for expanded row panels (makes them scrollable) |
144
+ | `accentColor` | `string` | `'#1890ff'` | Color used for sort icons, selected rows, resize line, etc. |
145
+ | `className` | `string` | `''` | Class name for the outer wrapper |
146
+ | `classNames` | `ClassNamesTypes` | `{}` | Granular class overrides per table region |
147
+ | `styles` | `StylesTypes` | `{}` | Inline style overrides per table region |
148
+ | `icons` | `BoltTableIcons` | — | Custom icon overrides for built-in SVG icons |
149
+ | `gripIcon` | `ReactNode` | — | Custom drag grip icon (deprecated, use `icons.gripVertical`) |
150
+ | `hideGripIcon` | `boolean` | `false` | Hide the drag grip icon from all headers |
151
+ | `pagination` | `PaginationType \| false` | — | Pagination config, or `false` to disable |
152
+ | `onPaginationChange` | `(page, pageSize) => void` | — | Called when page or page size changes |
153
+ | `onColumnResize` | `(columnKey, newWidth) => void` | — | Called when a column is resized |
154
+ | `onColumnOrderChange` | `(newOrder) => void` | — | Called when columns are reordered |
155
+ | `onColumnPin` | `(columnKey, pinned) => void` | — | Called when a column is pinned/unpinned |
156
+ | `onColumnHide` | `(columnKey, hidden) => void` | — | Called when a column is hidden/shown |
157
+ | `rowSelection` | `RowSelectionConfig<T>` | — | Row selection config |
158
+ | `rowPinning` | `RowPinningConfig` | — | Row pinning config (`{ top?: Key[], bottom?: Key[] }`) |
159
+ | `onRowPin` | `(rowKey, pinned) => void` | — | Called when a row is pinned/unpinned via cell context menu |
160
+ | `expandable` | `ExpandableConfig<T>` | — | Expandable row config |
161
+ | `onEndReached` | `() => void` | — | Called when scrolled near the bottom (infinite scroll) |
162
+ | `onEndReachedThreshold` | `number` | `5` | Rows from end to trigger `onEndReached` |
163
+ | `isLoading` | `boolean` | `false` | Shows shimmer skeleton rows when `true` |
164
+ | `onSortChange` | `(columnKey, direction) => void` | — | Server-side sort handler (disables local sort) |
165
+ | `onFilterChange` | `(filters) => void` | — | Server-side filter handler (disables local filter) |
166
+ | `columnContextMenuItems` | `ColumnContextMenuItem[]` | — | Custom items appended to the header context menu |
167
+ | `autoHeight` | `boolean` | `true` | Auto-size table height to content (capped at 10 rows) |
168
+ | `layoutLoading` | `boolean` | `false` | Show full skeleton layout (headers + rows) |
169
+ | `emptyRenderer` | `ReactNode` | — | Custom empty state content |
170
+ | `rowClassName` | `(record, index) => string` | — | Returns a CSS class name for conditional row styling |
171
+ | `rowStyle` | `(record, index) => CSSProperties` | — | Returns inline styles for conditional row styling |
172
+ | `disabledFilters` | `boolean` | `false` | Removes the filter option from all header context menus |
173
+ | `onCopy` | `(text, columnKey, record, rowIndex) => void` | — | Called after a cell value is copied to the clipboard |
174
+ | `keepPinnedRowsAcrossPages` | `boolean` | `false` | Pinned rows remain visible after navigating to a different page |
175
+ | `onEdit` | `(value, record, dataIndex, rowIndex) => void` | — | Called when a user finishes editing an editable cell |
176
+ | `onRowClick` | `(record, index, event) => void` | — | Called when a row is clicked; adds pointer cursor to all cells |
177
+ | `enableColumnVirtualization` | `boolean` | `false` | Only render columns visible in the viewport. Recommended for 100+ column tables |
178
+ | `enableDynamicRowHeight` | `boolean` | `false` | Measure each row's actual content height instead of using a fixed `rowHeight` |
179
+ | `columnPersistence` | `ColumnPersistenceConfig \| false` | `false` | Save column order, widths, visibility, and pinned state to localStorage |
180
+ | `showColumnSettings` | `boolean` | `true` | Show the column picker button (checklist panel to toggle columns on/off) |
181
+ | `hideGlobalSearch` | `boolean` | `false` | Hide the global search input above the table |
182
+ | `globalSearchValue` | `string` | — | Controlled global search value |
183
+ | `onGlobalSearchChange` | `(value: string) => void` | — | Called when the global search input changes |
184
+
185
+ ---
186
+
187
+ ### `ColumnType<T>`
188
+
189
+ | Field | Type | Default | Description |
190
+ |-------|------|---------|-------------|
191
+ | `key` | `string` | — | Unique column identifier (required) |
192
+ | `dataIndex` | `string` | — | Row object property to display (required for leaf columns, omit for groups) |
193
+ | `title` | `string \| ReactNode` | — | Header label (required) |
194
+ | `width` | `number` | `150` | Column width in pixels |
195
+ | `render` | `(value, record, index) => ReactNode` | — | Custom cell renderer |
196
+ | `shimmerRender` | `() => ReactNode` | — | Custom shimmer skeleton for this column |
197
+ | `sortable` | `boolean` | `true` | Show sort controls for this column |
198
+ | `sorter` | `boolean \| (a: T, b: T) => number` | — | Custom sort comparator for client-side sort |
199
+ | `filterable` | `boolean` | `true` | Show filter option in context menu |
200
+ | `filterFn` | `(value, record, dataIndex) => boolean` | — | Custom filter predicate for client-side filter |
201
+ | `hidden` | `boolean` | `false` | Hide this column |
202
+ | `defaultHidden` | `boolean` | `false` | Hide this column on first render (uncontrolled) |
203
+ | `pinned` | `'left' \| 'right' \| false` | `false` | Pin this column to an edge |
204
+ | `defaultPinned` | `'left' \| 'right' \| false` | `false` | Pin this column on first render (uncontrolled) |
205
+ | `className` | `string` | — | Class applied to all cells in this column |
206
+ | `style` | `CSSProperties` | — | Styles applied to all cells in this column |
207
+ | `copy` | `boolean \| (value, record, index) => string` | — | Enable "Copy" in cell context menu; function customizes what's copied |
208
+ | `editable` | `boolean` | `false` | Cells become inline-editable (no custom `render` required) |
209
+ | `children` | `ColumnType<T>[]` | — | Nested child columns. Makes this column a header group — only leaf columns render data |
210
+
211
+ ---
212
+
213
+ ### `ColumnPersistenceConfig`
214
+
215
+ | Field | Type | Default | Description |
216
+ |-------|------|---------|-------------|
217
+ | `storageKey` | `string` | — | localStorage key prefix used to store the state (required) |
218
+ | `persistOrder` | `boolean` | `true` | Persist and restore column order |
219
+ | `persistWidths` | `boolean` | `true` | Persist and restore column widths |
220
+ | `persistVisibility` | `boolean` | `true` | Persist and restore hidden/visible state |
221
+ | `persistPinned` | `boolean` | `true` | Persist and restore column pinned state |
222
+
223
+ ---
224
+
225
+ ## Examples
226
+
227
+ ### Global search
228
+
229
+ BoltTable renders a search bar above the table by default. It searches across **all columns** of every row using a case-insensitive substring match.
230
+
231
+ ```tsx
232
+ // Uncontrolled — BoltTable manages the search value internally
233
+ <BoltTable columns={columns} data={data} />
234
+
235
+ // Hide it entirely
236
+ <BoltTable columns={columns} data={data} hideGlobalSearch />
237
+
238
+ // Controlled — drive the search value from outside
239
+ const [search, setSearch] = useState('');
240
+
241
+ <BoltTable
242
+ columns={columns}
243
+ data={data}
244
+ globalSearchValue={search}
245
+ onGlobalSearchChange={setSearch}
246
+ />
247
+ ```
248
+
249
+ ---
250
+
251
+ ### Column picker
252
+
253
+ The column picker button ("Columns") is shown in the toolbar by default. Clicking it opens a checklist panel letting users toggle any non-pinned column on or off.
254
+
255
+ ```tsx
256
+ // Shown by default — no configuration needed
257
+ <BoltTable columns={columns} data={data} />
258
+
259
+ // Hide the column picker button
260
+ <BoltTable columns={columns} data={data} showColumnSettings={false} />
261
+ ```
262
+
263
+ > **Note:** Pinned columns cannot be hidden from the picker — unpin them first.
264
+
265
+ ---
266
+
267
+ ### Column persistence
268
+
269
+ Pass a `columnPersistence` config to automatically save column state to `localStorage`. On the next page load the table restores the saved order, widths, visibility, and pinned state.
270
+
271
+ ```tsx
272
+ <BoltTable
273
+ columns={columns}
274
+ data={data}
275
+ rowKey="id"
276
+ columnPersistence={{
277
+ storageKey: 'users-table', // stored as bt_users-table in localStorage
278
+ persistOrder: true, // default
279
+ persistWidths: true, // default
280
+ persistVisibility: true, // default
281
+ persistPinned: true, // default
282
+ }}
283
+ />
284
+ ```
285
+
286
+ To persist only widths (not order or visibility):
287
+
288
+ ```tsx
289
+ <BoltTable
290
+ columnPersistence={{
291
+ storageKey: 'orders-table',
292
+ persistOrder: false,
293
+ persistVisibility: false,
294
+ persistPinned: false,
295
+ }}
296
+ />
297
+ ```
298
+
299
+ ---
300
+
301
+ ### Horizontal virtualization
302
+
303
+ For tables with a large number of columns enable column virtualization. Only the columns currently visible in the viewport (plus one overscan column on each side) are rendered. Pinned columns always render regardless.
304
+
305
+ ```tsx
306
+ <BoltTable
307
+ columns={hundredsOfColumns}
308
+ data={data}
309
+ enableColumnVirtualization
310
+ />
311
+ ```
312
+
313
+ > Best suited for tables with 50+ columns. For typical column counts (< 30) the overhead isn't worth it.
314
+
315
+ ---
316
+
317
+ ### Dynamic row heights
318
+
319
+ When row content can vary in height (e.g. multi-line text, embedded components), enable dynamic row heights. BoltTable uses `ResizeObserver` to measure each row's actual rendered height and updates the virtualizer accordingly.
320
+
321
+ ```tsx
322
+ <BoltTable
323
+ columns={columns}
324
+ data={data}
325
+ enableDynamicRowHeight
326
+ rowHeight={40} // used as the minimum / estimated height
327
+ />
328
+ ```
329
+
330
+ The `rowHeight` prop still acts as the minimum and estimated height used before measurement. Rows grow taller as needed based on their content.
331
+
332
+ ---
333
+
334
+ ### Duplicate row keys
335
+
336
+ If your data can contain rows with the same `id` (or whatever field is used as `rowKey`), BoltTable handles it automatically — no extra configuration needed. Internally it detects duplicates and appends the row index to produce a unique key for the virtualizer and DOM, while still passing the original key to selection and event callbacks.
337
+
338
+ ```tsx
339
+ // Works correctly even with duplicate ids
340
+ <BoltTable
341
+ columns={columns}
342
+ data={[
343
+ { id: 1, name: 'Alice' },
344
+ { id: 1, name: 'Alice (copy)' }, // same id — renders correctly
345
+ ]}
346
+ rowKey="id"
347
+ />
348
+ ```
349
+
350
+ ---
351
+
352
+ ### Sorting
353
+
354
+ **Client-side** (no `onSortChange` — BoltTable sorts locally):
355
+
356
+ ```tsx
357
+ const columns: ColumnType<User>[] = [
358
+ {
359
+ key: 'name',
360
+ dataIndex: 'name',
361
+ title: 'Name',
362
+ sortable: true,
363
+ sorter: (a, b) => a.name.localeCompare(b.name),
364
+ },
365
+ {
366
+ key: 'age',
367
+ dataIndex: 'age',
368
+ title: 'Age',
369
+ sortable: true,
370
+ },
371
+ ];
372
+
373
+ <BoltTable columns={columns} data={data} />
374
+ ```
375
+
376
+ **Server-side** (provide `onSortChange` — BoltTable delegates to you):
377
+
378
+ ```tsx
379
+ const [sortKey, setSortKey] = useState('');
380
+ const [sortDir, setSortDir] = useState<SortDirection>(null);
381
+
382
+ <BoltTable
383
+ columns={columns}
384
+ data={serverData}
385
+ onSortChange={(key, dir) => {
386
+ setSortKey(key);
387
+ setSortDir(dir);
388
+ refetch({ sortKey: key, sortDir: dir });
389
+ }}
390
+ />
391
+ ```
392
+
393
+ ---
394
+
395
+ ### Filtering
396
+
397
+ **Client-side** (no `onFilterChange`):
398
+
399
+ ```tsx
400
+ const columns: ColumnType<User>[] = [
401
+ {
402
+ key: 'status',
403
+ dataIndex: 'status',
404
+ title: 'Status',
405
+ filterable: true,
406
+ filterFn: (value, record) => record.status === value,
407
+ },
408
+ ];
409
+ ```
410
+
411
+ **Server-side**:
412
+
413
+ ```tsx
414
+ <BoltTable
415
+ columns={columns}
416
+ data={serverData}
417
+ onFilterChange={(filters) => {
418
+ setActiveFilters(filters);
419
+ refetch({ filters });
420
+ }}
421
+ />
422
+ ```
423
+
424
+ ---
425
+
426
+ ### Pagination
427
+
428
+ **Client-side** (pass all data, BoltTable slices it):
429
+
430
+ ```tsx
431
+ <BoltTable
432
+ columns={columns}
433
+ data={allUsers}
434
+ pagination={{ pageSize: 20 }}
435
+ onPaginationChange={(page, size) => setPage(page)}
436
+ />
437
+ ```
438
+
439
+ **Server-side** (pass only the current page):
440
+
441
+ ```tsx
442
+ <BoltTable
443
+ columns={columns}
444
+ data={currentPageData}
445
+ pagination={{
446
+ current: page,
447
+ pageSize: 20,
448
+ total: 500,
449
+ showTotal: (total, [from, to]) => `${from}-${to} of ${total} users`,
450
+ }}
451
+ onPaginationChange={(page, size) => fetchPage(page, size)}
452
+ />
453
+ ```
454
+
455
+ **Disable pagination:**
456
+
457
+ ```tsx
458
+ <BoltTable columns={columns} data={data} pagination={false} />
459
+ ```
460
+
461
+ ---
462
+
463
+ ### Row selection
464
+
465
+ ```tsx
466
+ const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
467
+
468
+ <BoltTable
469
+ columns={columns}
470
+ data={data}
471
+ rowKey="id"
472
+ rowSelection={{
473
+ type: 'checkbox',
474
+ selectedRowKeys,
475
+ onChange: (keys, rows) => setSelectedRowKeys(keys),
476
+ getCheckboxProps: (record) => ({
477
+ disabled: record.status === 'locked',
478
+ }),
479
+ }}
480
+ />
481
+ ```
482
+
483
+ ---
484
+
485
+ ### Expandable rows
486
+
487
+ ```tsx
488
+ <BoltTable
489
+ columns={columns}
490
+ data={data}
491
+ rowKey="id"
492
+ expandable={{
493
+ rowExpandable: (record) => record.details !== null,
494
+ expandedRowRender: (record) => (
495
+ <div style={{ padding: 16 }}>
496
+ <h4>{record.name} — Details</h4>
497
+ <pre>{JSON.stringify(record.details, null, 2)}</pre>
498
+ </div>
499
+ ),
500
+ }}
501
+ expandedRowHeight={150}
502
+ maxExpandedRowHeight={400}
503
+ />
504
+ ```
505
+
506
+ ---
507
+
508
+ ### Infinite scroll
509
+
510
+ ```tsx
511
+ const [data, setData] = useState<User[]>([]);
512
+ const [isLoading, setIsLoading] = useState(false);
513
+
514
+ const loadMore = async () => {
515
+ setIsLoading(true);
516
+ const newRows = await fetchNextPage();
517
+ setData(prev => [...prev, ...newRows]);
518
+ setIsLoading(false);
519
+ };
520
+
521
+ <BoltTable
522
+ columns={columns}
523
+ data={data}
524
+ isLoading={isLoading}
525
+ onEndReached={loadMore}
526
+ onEndReachedThreshold={8}
527
+ pagination={false}
528
+ />
529
+ ```
530
+
531
+ ---
532
+
533
+ ### Column pinning
534
+
535
+ ```tsx
536
+ const columns: ColumnType<User>[] = [
537
+ { key: 'name', dataIndex: 'name', title: 'Name', pinned: 'left', width: 200 },
538
+ { key: 'email', dataIndex: 'email', title: 'Email', width: 250 },
539
+ { key: 'actions', dataIndex: 'actions', title: 'Actions', pinned: 'right', width: 100 },
540
+ ];
541
+ ```
542
+
543
+ Users can also pin/unpin columns at runtime via the right-click context menu.
544
+
545
+ ---
546
+
547
+ ### Row pinning
548
+
549
+ 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.
550
+
551
+ ```tsx
552
+ const [rowPinning, setRowPinning] = useState({ top: [], bottom: [] });
553
+
554
+ <BoltTable
555
+ columns={columns}
556
+ data={data}
557
+ rowKey="id"
558
+ rowPinning={rowPinning}
559
+ onRowPin={(key, pinned) => {
560
+ setRowPinning(prev => {
561
+ const top = (prev.top ?? []).filter(k => String(k) !== String(key));
562
+ const bottom = (prev.bottom ?? []).filter(k => String(k) !== String(key));
563
+ if (pinned === 'top') top.push(key);
564
+ if (pinned === 'bottom') bottom.push(key);
565
+ return { top, bottom };
566
+ });
567
+ }}
568
+ styles={{ pinnedRowBg: 'rgba(255, 255, 255, 0.95)' }}
569
+ />
570
+ ```
571
+
572
+ Users can also pin/unpin rows at runtime via the right-click context menu on any body cell (when `onRowPin` is provided).
573
+
574
+ 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`.
575
+
576
+ ---
577
+
578
+ ### Cell context menu & copy
579
+
580
+ Right-click (or long-press on mobile) any body cell to see a context menu with:
581
+
582
+ - **Pin to Top / Unpin from Top** — shown when `onRowPin` is provided
583
+ - **Pin to Bottom / Unpin from Bottom** — shown when `onRowPin` is provided
584
+ - **Copy** — shown when the column has `copy: true` or a copy function
585
+
586
+ ```tsx
587
+ const columns: ColumnType<User>[] = [
588
+ {
589
+ key: 'name',
590
+ dataIndex: 'name',
591
+ title: 'Name',
592
+ copy: true, // copies the raw cell value
593
+ },
594
+ {
595
+ key: 'email',
596
+ dataIndex: 'email',
597
+ title: 'Email',
598
+ // Custom copy — control exactly what goes to the clipboard
599
+ copy: (value, record) => `${record.name} <${value}>`,
600
+ },
601
+ ];
602
+ ```
603
+
604
+ 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.
605
+
606
+ ---
607
+
608
+ ### Editable cells
609
+
610
+ Mark columns as `editable: true` and provide an `onEdit` callback to allow inline editing. Right-click (or long-press on mobile) an editable cell to see the **Edit** option (with a pencil icon) in the context menu. Selecting it turns the cell into an input field. Press **Enter** or click away to commit, **Escape** to cancel.
611
+
612
+ ```tsx
613
+ const [data, setData] = useState<User[]>(initialData);
614
+
615
+ const columns: ColumnType<User>[] = [
616
+ { key: 'name', dataIndex: 'name', title: 'Name', editable: true, width: 200 },
617
+ { key: 'age', dataIndex: 'age', title: 'Age', editable: true, width: 80 },
618
+ {
619
+ key: 'status',
620
+ dataIndex: 'status',
621
+ title: 'Status',
622
+ render: (value) => <span className="badge">{String(value)}</span>,
623
+ editable: true, // ignored — custom render takes precedence
624
+ },
625
+ ];
626
+
627
+ <BoltTable
628
+ columns={columns}
629
+ data={data}
630
+ rowKey="id"
631
+ onEdit={(value, record, dataIndex, rowIndex) => {
632
+ setData(prev =>
633
+ prev.map((row, i) =>
634
+ i === rowIndex ? { ...row, [dataIndex]: value } : row
635
+ )
636
+ );
637
+ }}
638
+ />
639
+ ```
640
+
641
+ The edit icon can be customized via the `icons` prop:
642
+
643
+ ```tsx
644
+ <BoltTable icons={{ edit: <MyPencilIcon size={14} /> }} />
645
+ ```
646
+
647
+ > **Note:** `editable` is skipped for columns that define a custom `render` function — since the cell content is fully controlled by the renderer, inline editing wouldn't know how to display or commit changes. If you need editable custom-rendered cells, handle the editing UX inside your `render` function.
648
+
649
+ ---
650
+
651
+ ### Styling overrides
652
+
653
+ ```tsx
654
+ <BoltTable
655
+ columns={columns}
656
+ data={data}
657
+ accentColor="#6366f1"
658
+ classNames={{
659
+ header: 'text-xs uppercase tracking-wider text-gray-500',
660
+ cell: 'text-sm',
661
+ pinnedHeader: 'border-r border-indigo-200',
662
+ pinnedCell: 'border-r border-indigo-100',
663
+ }}
664
+ styles={{
665
+ header: { fontWeight: 600 },
666
+ rowHover: { backgroundColor: '#f0f9ff' },
667
+ rowSelected: { backgroundColor: '#e0e7ff' },
668
+ pinnedBg: 'rgba(238, 242, 255, 0.95)',
669
+ }}
670
+ />
671
+ ```
672
+
673
+ ---
674
+
675
+ ### Nested / grouped columns
676
+
677
+ Group related columns under a shared header. Parent columns act as header groups — only leaf columns (without `children`) render data cells. Leaf columns within groups support resizing and reordering just like standalone columns.
678
+
679
+ ```tsx
680
+ const columns: ColumnType<User>[] = [
681
+ { key: 'id', dataIndex: 'id', title: 'ID', width: 80 },
682
+ {
683
+ key: 'nameGroup',
684
+ title: 'Name',
685
+ children: [
686
+ { key: 'firstName', dataIndex: 'firstName', title: 'First Name', width: 150 },
687
+ { key: 'lastName', dataIndex: 'lastName', title: 'Last Name', width: 150 },
688
+ ],
689
+ },
690
+ {
691
+ key: 'contactGroup',
692
+ title: 'Contact Info',
693
+ children: [
694
+ { key: 'email', dataIndex: 'email', title: 'Email', width: 250 },
695
+ { key: 'phone', dataIndex: 'phone', title: 'Phone', width: 150 },
696
+ ],
697
+ },
698
+ { key: 'age', dataIndex: 'age', title: 'Age', width: 80 },
699
+ ];
700
+
701
+ <BoltTable columns={columns} data={data} rowKey="id" />
702
+ ```
703
+
704
+ The header renders two rows: group headers span their children columns in the top row, and leaf column headers appear in the bottom row. Standalone columns (not in any group) span both header rows.
705
+
706
+ ---
707
+
708
+ ### Pagination styles
709
+
710
+ Customize every part of the pagination footer via `styles` and `classNames`:
711
+
712
+ ```tsx
713
+ <BoltTable
714
+ columns={columns}
715
+ data={data}
716
+ pagination={{ pageSize: 20 }}
717
+ classNames={{
718
+ pagination: 'bg-gray-50 border-t border-gray-200',
719
+ paginationButton: 'rounded hover:bg-gray-200',
720
+ paginationActiveButton: 'font-bold text-blue-600',
721
+ paginationSelect: 'rounded border-gray-300',
722
+ paginationInfo: 'text-gray-500 text-xs',
723
+ }}
724
+ styles={{
725
+ pagination: { height: 40 },
726
+ paginationButton: { borderRadius: 4 },
727
+ paginationActiveButton: { fontWeight: 700 },
728
+ paginationSelect: { borderRadius: 4 },
729
+ paginationInfo: { fontSize: 11 },
730
+ }}
731
+ />
732
+ ```
733
+
734
+ Available pagination style/class keys: `pagination` (wrapper), `paginationButton` (nav buttons), `paginationActiveButton` (active page), `paginationSelect` (page-size dropdown), `paginationInfo` (range text).
735
+
736
+ ---
737
+
738
+ ### Fixed height (fill parent)
739
+
740
+ By default, BoltTable auto-sizes to its content. To fill a fixed-height container instead:
741
+
742
+ ```tsx
743
+ <div style={{ height: 600 }}>
744
+ <BoltTable
745
+ columns={columns}
746
+ data={data}
747
+ autoHeight={false}
748
+ />
749
+ </div>
750
+ ```
751
+
752
+ ---
753
+
754
+ ## Documentation
755
+
756
+ For the complete guide with in-depth examples for every feature, visit the **[BoltTable Documentation](https://bolt-table.vercel.app/)**.
757
+
758
+ ---
759
+
760
+ ## Type exports
761
+
762
+ ```ts
763
+ import type {
764
+ ColumnType,
765
+ ColumnContextMenuItem,
766
+ ColumnPersistenceConfig,
767
+ RowSelectionConfig,
768
+ RowPinningConfig,
769
+ ExpandableConfig,
770
+ PaginationType,
771
+ SortDirection,
772
+ DataRecord,
773
+ BoltTableIcons,
774
+ } from 'bolt-table';
775
+ ```
776
+
777
+ ---
778
+
779
+ ## License
780
+
781
+ MIT © [Venkatesh Sirigineedi](https://github.com/venkateshwebdev)
782
+
4
783
 
5
784
  [![npm version](https://img.shields.io/npm/v/bolt-table)](https://www.npmjs.com/package/bolt-table)
6
785
  [![license](https://img.shields.io/npm/l/bolt-table)](./LICENSE)