@pulog/component 0.0.0-stage → 0.1.0

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,3 +1,816 @@
1
- # Temporary Holding Version
1
+ # @pulog/component
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The shared **business components** for TMS apps. Everything is imported from one place:
4
+ `import { DataTable, ... } from '@pulog/component'`. Each component family lives in its own folder under `src/` and is
5
+ re-exported from `src/index.ts`; adding one (e.g. shipment/order cards) is described in `docs/CONTRIBUTING.md`.
6
+
7
+ Currently available: **DataTable** (this document). Primitives stay in `@pulog/ui`.
8
+
9
+ ---
10
+
11
+ ## DataTable
12
+
13
+ The shared DataTable component family: `DataTable`, its contexts (filter / pagination /
14
+ selection / sort / column visibility / column pinning), filters, pagination, selection helpers,
15
+ query-building utilities (`buildQueryParams`, `applyFilters`, ...) and the default English strings.
16
+
17
+ Extracted from `tms_billing` (the pilot). Source of truth is **this package** - never copy it into an app.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @pulog/component @pulog/ui @pulog/utils
23
+ ```
24
+
25
+ Peer dependencies (the app already has them; the package never bundles them):
26
+ `react`, `react-dom`, `lucide-react`, `date-fns`, `react-day-picker`, `framer-motion`, `react-i18next`.
27
+ `@pulog/ui` and `@pulog/utils` are regular dependencies of this package, but install them in the app too
28
+ when you import from them directly.
29
+
30
+ ## App setup (once per app)
31
+
32
+ 1. **Tailwind must scan the package.** The package ships compiled JS without CSS, so add to the app's
33
+ `src/index.css` (path relative to that file):
34
+ ```css
35
+ @source "../node_modules/@pulog";
36
+ ```
37
+ 2. **Translations.** Components call `useTranslation('datatable')`. Register the defaults:
38
+ ```ts
39
+ import { datatableEn, DATATABLE_NS } from '@pulog/component'
40
+ // add `datatable: datatableEn` to your i18n resources and keep 'datatable' in `ns`
41
+ ```
42
+ Override any key by merging your own object over `datatableEn`.
43
+ 3. **Portals / Shadow DOM.** Popovers, selects and dropdowns inside the table render through
44
+ `PortalContainerContext` from `@pulog/ui`. The app must provide that context at its Shadow DOM root
45
+ (see `tms_host/REMOTE_STYLE_GUIDE.md`) and must import the context from `@pulog/ui` (or a re-export of it),
46
+ never define its own - two contexts means portals escape the shadow tree.
47
+
48
+ ## Exports
49
+
50
+ - Components: `DataTable`, `DataTableProvider`, `SearchFilter`, `DateFilter`, `DateRangeFilter`, `SelectFilter`,
51
+ `MultiSelectFilter`, `CheckboxFilter`, `RangeFilter`, `FilterPanel`, `ColumnHeaderFilter`, `DateTreeFilter`,
52
+ `TraditionalPagination`, `InfiniteScrollPagination`, `RowCheckbox`, `SelectAllCheckbox`, `ExpandableRow`,
53
+ `SelectionSummary`, `ColumnPinMenu`, `ColumnVisibilityPanel`.
54
+ - Context providers/hooks: `FilterProvider/useFilters`, `PaginationProvider/usePagination`,
55
+ `SelectionProvider/useSelection`, `SortProvider/useSort`, `ColumnVisibilityProvider/useColumnVisibility`,
56
+ `ColumnPinningProvider/useColumnPinning`.
57
+ - Types: `DataTableProps`, `DataTableColumn`, `DataTableState` (table level, used by `onStateChange`),
58
+ `DataTableQueryState` (generic query-state shape; this is `DataTableState<T>` from the old `lib/data-table/types`),
59
+ `FilterValue`, `FilterType`, `ColumnFilter`, `SortState`, `PaginationMeta`, `RowSelectionState`, ...
60
+ - Utilities: everything in `src/lib` (`buildQueryParams`, `paramsToURLSearchParams`, `buildURL`, `applyFilters`,
61
+ `removeFilter`, `hasActiveFilters`, `formatDateKey`, `parseDateValue`, ...).
62
+ - i18n: `datatableEn`, `DATATABLE_NS`.
63
+
64
+ `mock-data` (sample users for demos) is intentionally **not** shipped.
65
+
66
+ ## Changing this package
67
+
68
+ See the repo-level `docs/CONTRIBUTING.md`. Source: `src/`. Tests: `npm test -w @pulog/component`.
69
+ Known issue: `src/DataTable.test.tsx` is inherited from tms_billing, stale (32 failing cases) and excluded in
70
+ `vitest.config.ts` until it is rewritten.
71
+
72
+ ---
73
+
74
+ # Component guide (carried over from tms_billing)
75
+
76
+ Imports below use the package name. Everything else is unchanged from the original guide.
77
+
78
+ # Feature-Rich DataTable Component
79
+
80
+ A comprehensive, production-ready DataTable component built with React 18, TypeScript, and shadcn/ui. Includes advanced filtering, sorting, selection, pagination, and expandable rows.
81
+
82
+ ## Features
83
+
84
+ ### Filtering System (7 Filter Types)
85
+ - **Search Filter**: Full-text search across fields
86
+ - **Date Filter**: Single date selection
87
+ - **Date Range Filter**: Date range filtering
88
+ - **Select Filter**: Single-select dropdown
89
+ - **Multi-Select Filter**: Multiple selection with badges
90
+ - **Checkbox Filter**: Boolean toggle filtering
91
+ - **Range Filter**: Numeric range slider
92
+
93
+ ### Selection & Expansion
94
+ - Row checkboxes with individual selection
95
+ - Select all / deselect all functionality
96
+ - Selection summary with action buttons
97
+ - Expandable rows with custom content renderer
98
+ - Bulk action support for selected rows
99
+
100
+ ### Pagination
101
+ - **Traditional Mode**: Page navigation with first/last/prev/next buttons
102
+ - **Infinite Scroll Mode**: Load More button for infinite scrolling
103
+ - Configurable page sizes (10, 25, 50, 100)
104
+ - Total items and current range display
105
+
106
+ ### Sorting
107
+ - Multi-column sortable headers
108
+ - Ascending/Descending/None toggle
109
+ - Visual sort indicators
110
+ - Server-side ready query building
111
+
112
+ ### Additional Features
113
+ - Top-level optional search bar
114
+ - Scrollable table area with configurable fixed height
115
+ - Sticky table header
116
+ - Sticky columns on the left with automatic offset stacking
117
+ - Column pinning — pin/unpin left or right at runtime via a per-header menu (in addition to a column's static `sticky` config)
118
+ - Column visibility toggle
119
+ - Loading skeleton states
120
+ - Error state handling
121
+ - Empty state message
122
+ - Responsive design
123
+ - Client & server-side ready architecture
124
+ - React Context for state management (no external state library)
125
+
126
+ ## Installation & Setup
127
+
128
+ ### 1. Basic Setup
129
+
130
+ `<DataTable>` is self-contained — it owns its filter/pagination/selection/sort/
131
+ column-visibility state internally, so there's no separate provider to render.
132
+ Just pass props:
133
+
134
+ ```tsx
135
+ 'use client';
136
+
137
+ import { DataTable } from '@pulog/component';
138
+
139
+ export default function MyPage() {
140
+ return (
141
+ <DataTable
142
+ columns={columns}
143
+ data={data}
144
+ rowKey="id"
145
+ pageSize={10} // initial rows-per-page
146
+ selectionMode="checkbox" // or "radio" — only needed with showSelection
147
+ defaultColumnVisibility={{}} // initial per-column visibility
148
+ />
149
+ );
150
+ }
151
+ ```
152
+
153
+ `DataTableProvider` still exists (`./contexts/DataTableProvider`) but is an
154
+ internal implementation detail of `<DataTable>` — page code should never need
155
+ to render it directly.
156
+
157
+ **Escape hatch:** the one legitimate reason to render `DataTableProvider`
158
+ yourself is when a *sibling* component (rendered next to `<DataTable>`, not
159
+ passed into it via `toolbarLeft`/`toolbarRight`) needs to read the table's
160
+ selection/filter state — e.g. a "Process selected" button above the table
161
+ that calls `useSelection()`. Wrap both in one explicit `<DataTableProvider>`;
162
+ `<DataTable>` detects that ambient provider and reuses it instead of creating
163
+ a disconnected one of its own:
164
+
165
+ ```tsx
166
+ function ProcessSelectedButton() {
167
+ const { selectedCount, getSelectedRowIds } = useSelection(); // shares state with the table below
168
+ return <Button disabled={!selectedCount}>Process {selectedCount}</Button>;
169
+ }
170
+
171
+ <DataTableProvider defaultPageSize={10}>
172
+ <ProcessSelectedButton />
173
+ <DataTable columns={columns} data={data} rowKey="id" pageSize={10} />
174
+ </DataTableProvider>
175
+ ```
176
+
177
+ Keep `defaultPageSize` (and `selectionMode` / `defaultColumnVisibility` if
178
+ used) on the outer `DataTableProvider` in sync with what you'd otherwise pass
179
+ to `<DataTable>` — with an ambient provider present, `<DataTable>`'s own
180
+ `pageSize`/`selectionMode`/`defaultColumnVisibility` props are ignored (the
181
+ ambient provider already owns that state).
182
+
183
+ A nested `<DataTable>` that must stay **isolated** from an outer table (e.g.
184
+ a detail table inside an expanded row, with its own independent pagination)
185
+ should keep its own explicit `<DataTableProvider>` wrapper too — nesting a
186
+ provider always shadows the ambient one for its own subtree, giving it a
187
+ fresh, separate state scope.
188
+
189
+ ### 2. Define Column Configuration
190
+
191
+ ```tsx
192
+ import { DataTableColumn } from '@pulog/component';
193
+
194
+ interface User {
195
+ id: number;
196
+ name: string;
197
+ email: string;
198
+ role: string;
199
+ }
200
+
201
+ const columns: DataTableColumn<User>[] = [
202
+ {
203
+ id: 'name',
204
+ header: 'Name',
205
+ accessor: 'name',
206
+ sortable: true,
207
+ filterable: true,
208
+ sticky: 'left', // or `right`, or `true` for left
209
+ filterConfig: {
210
+ id: 'name',
211
+ label: 'Search Name',
212
+ type: 'search',
213
+ placeholder: 'Enter name...',
214
+ },
215
+ },
216
+ {
217
+ id: 'email',
218
+ header: 'Email',
219
+ accessor: 'email',
220
+ sortable: true,
221
+ },
222
+ {
223
+ id: 'role',
224
+ header: 'Role',
225
+ accessor: 'role',
226
+ sortable: true,
227
+ filterable: true,
228
+ filterConfig: {
229
+ id: 'role',
230
+ label: 'Select Role',
231
+ type: 'multiSelect',
232
+ options: [
233
+ { label: 'Admin', value: 'admin' },
234
+ { label: 'User', value: 'user' },
235
+ ],
236
+ },
237
+ },
238
+ ];
239
+ ```
240
+
241
+ Sticky columns can be added to multiple visible columns. The table measures each sticky column at runtime and stacks their offsets so they do not overlap.
242
+ Use `sticky: 'left'` or `sticky: 'right'` to pin columns on either side by default.
243
+
244
+ ### Runtime Column Pinning
245
+
246
+ Each header cell (except tool columns) shows a pin icon that opens a menu with "Pin ke Kiri" / "Pin ke Kanan" / "Lepas Pin" — this lets users pin/unpin any column at runtime, overriding its static `sticky` config for that session. Pinned columns are automatically reordered to the corresponding edge (left-pinned first, then unpinned, then right-pinned), each group keeping its own relative order.
247
+
248
+ - Disable the menu for the whole table: `enableColumnPinning={false}`
249
+ - Disable it for one column: `pinnable: false` on that column
250
+ - Seed initial runtime pin state: `defaultColumnPinning={{ mst_client_id: 'left' }}` (falls back to each column's `sticky` for ids left unset)
251
+
252
+ ### 3. Render DataTable
253
+
254
+ ```tsx
255
+ <DataTable<User>
256
+ columns={columns}
257
+ data={users}
258
+ rowKey="id"
259
+ showSelection={true}
260
+ showExpand={true}
261
+ expandContent={(row) => <div>Details for {row.name}</div>}
262
+ paginationMode="traditional"
263
+ showFilters={true}
264
+ showSearch={true}
265
+ searchFields={['name', 'email']} // optional, defaults to all columns
266
+ tableHeight={600}
267
+ pageSize={10}
268
+ />
269
+ ```
270
+
271
+ ## Usage Examples
272
+
273
+ ### Server-Side Integration
274
+
275
+ Enable server-side mode by setting the `manual*` props:
276
+
277
+ ```tsx
278
+ <DataTable
279
+ columns={columns}
280
+ data={data}
281
+ rowKey="id"
282
+ manualPagination
283
+ manualFiltering
284
+ manualSorting
285
+ totalCount={65}
286
+ onStateChange={handleStateChange}
287
+ />
288
+ ```
289
+
290
+ #### Using `onStateChange` Callback
291
+
292
+ The recommended approach — `onStateChange` fires whenever pagination, filter, sort, or search state changes:
293
+
294
+ ```tsx
295
+ const [data, setData] = useState([]);
296
+ const [isLoading, setIsLoading] = useState(false);
297
+ const [totalCount, setTotalCount] = useState(0);
298
+
299
+ const handleStateChange = useCallback(async (state: DataTableState) => {
300
+ setIsLoading(true);
301
+ try {
302
+ const params = buildQueryParams(
303
+ state.pageIndex,
304
+ state.pageSize,
305
+ state.filters,
306
+ state.sort,
307
+ undefined,
308
+ state.search
309
+ );
310
+ const response = await fetch(`/api/data?${paramsToURLSearchParams(params)}`);
311
+ const result = await response.json();
312
+ setData(result.data);
313
+ setTotalCount(result.meta.pagination.totalData);
314
+ } finally {
315
+ setIsLoading(false);
316
+ }
317
+ }, []);
318
+
319
+ <DataTable
320
+ columns={columns}
321
+ data={data}
322
+ isLoading={isLoading}
323
+ rowKey="id"
324
+ manualPagination
325
+ manualFiltering
326
+ manualSorting
327
+ totalCount={totalCount}
328
+ onStateChange={handleStateChange}
329
+ />
330
+ ```
331
+
332
+ #### Reading Table State From a Custom Toolbar Element
333
+
334
+ `useFilters` / `usePagination` / `useSort` / `useSelection` / `useColumnVisibility`
335
+ only work inside `<DataTable>`'s own subtree, since the table creates its own
336
+ context instance internally — a sibling component sitting next to `<DataTable>`
337
+ in JSX can't reach it. Fetching data for the table itself should use
338
+ `onStateChange` (above), not these hooks directly.
339
+
340
+ The one place these hooks are legitimately useful is a **custom toolbar
341
+ element** passed via `toolbarLeft` / `toolbarRight` — that content is rendered
342
+ inside `<DataTable>`'s own tree, so it shares the same context:
343
+
344
+ ```tsx
345
+ function ActiveDateRangeBadge() {
346
+ // Works: this component is rendered *inside* <DataTable> via toolbarLeft.
347
+ const { filters, removeFilter } = useFilters();
348
+ const active = filters.find((f) => f.field === 'tanggal_valid');
349
+ if (!active) return null;
350
+ return <Badge onClick={() => removeFilter('tanggal_valid')}>Tanggal aktif ✕</Badge>;
351
+ }
352
+
353
+ <DataTable
354
+ columns={columns}
355
+ data={data}
356
+ rowKey="id"
357
+ toolbarLeft={<ActiveDateRangeBadge />}
358
+ />
359
+ ```
360
+
361
+ ### Custom Cell Rendering
362
+
363
+ ```tsx
364
+ const columns: DataTableColumn<User>[] = [
365
+ {
366
+ id: 'status',
367
+ header: 'Status',
368
+ accessor: 'status',
369
+ cell: (row) => (
370
+ <Badge variant={row.status === 'active' ? 'default' : 'secondary'}>
371
+ {row.status}
372
+ </Badge>
373
+ ),
374
+ },
375
+ ];
376
+ ```
377
+
378
+ ### Expandable Rows with Details
379
+
380
+ ```tsx
381
+ <DataTable
382
+ columns={columns}
383
+ data={data}
384
+ rowKey="id"
385
+ showExpand={true}
386
+ expandContent={(row) => (
387
+ <div className="space-y-4">
388
+ <div className="grid grid-cols-2 gap-4">
389
+ <div>
390
+ <p className="text-xs text-muted-foreground">Email</p>
391
+ <p className="font-medium">{row.email}</p>
392
+ </div>
393
+ <div>
394
+ <p className="text-xs text-muted-foreground">Phone</p>
395
+ <p className="font-medium">{row.phone}</p>
396
+ </div>
397
+ </div>
398
+ <p>{row.description}</p>
399
+ </div>
400
+ )}
401
+ />
402
+ ```
403
+
404
+ ### Row Selection with Bulk Actions
405
+
406
+ ```tsx
407
+ import { useSelection } from '@pulog/component';
408
+
409
+ function DataTableWithActions() {
410
+ const { getSelectedRowIds } = useSelection();
411
+
412
+ const handleBulkDelete = async () => {
413
+ const selectedIds = getSelectedRowIds();
414
+ await fetch('/api/users/bulk-delete', {
415
+ method: 'POST',
416
+ body: JSON.stringify({ ids: selectedIds }),
417
+ });
418
+ };
419
+
420
+ return (
421
+ <>
422
+ <DataTable
423
+ columns={columns}
424
+ data={data}
425
+ rowKey="id"
426
+ showSelection={true}
427
+ />
428
+ <SelectionSummary
429
+ actionLabel="Delete Selected"
430
+ onAction={handleBulkDelete}
431
+ />
432
+ </>
433
+ );
434
+ }
435
+ ```
436
+
437
+ ## API Integration Reference
438
+
439
+ ### onStateChange Callback Payload
440
+
441
+ When any table state changes, `onStateChange` fires with this structure:
442
+
443
+ ```json
444
+ {
445
+ "pageIndex": 2,
446
+ "pageSize": 10,
447
+ "sort": {
448
+ "column": "name",
449
+ "order": "asc"
450
+ },
451
+ "filters": [
452
+ { "field": "status", "type": "select", "operator": "equals", "value": "active" },
453
+ { "field": "salary", "type": "range", "operator": "between", "value": [50000, 100000] }
454
+ ],
455
+ "search": {
456
+ "query": "john",
457
+ "fields": ["name", "email"]
458
+ }
459
+ }
460
+ ```
461
+
462
+ ### buildQueryParams Output
463
+
464
+ Use `buildQueryParams()` to convert state into API-friendly query parameters:
465
+
466
+ ```ts
467
+ import { buildQueryParams, paramsToURLSearchParams } from '@pulog/component';
468
+
469
+ const params = buildQueryParams(pageIndex, pageSize, filters, sort, additionalParams, search);
470
+ const urlParams = paramsToURLSearchParams(params);
471
+ // Use: fetch(`/api/data?${urlParams}`)
472
+ ```
473
+
474
+ #### GET Request — Query Parameters
475
+
476
+ ```
477
+ GET /api/data?pageIndex=2&pageSize=10&sortBy=name&sortOrder=asc&search=john&searchFields=name&searchFields=email&filters[status]=active&filters[salary][min]=50000&filters[salary][max]=100000
478
+ ```
479
+
480
+ Full parameter reference:
481
+
482
+ | Parameter | Type | Example | Description |
483
+ |---|---|---|---|
484
+ | `pageIndex` | number | `2` | Current page (1-indexed) |
485
+ | `pageSize` | number | `10` | Items per page |
486
+ | `sortBy` | string | `name` | Sort column ID |
487
+ | `sortOrder` | string | `asc` | Sort direction: `asc` or `desc` |
488
+ | `search` | string | `john` | Global search query |
489
+ | `searchFields` | string[] | `name,email` | Fields to search in |
490
+ | `filters[field]` | any | `active` | Simple filter value |
491
+ | `filters[field][min]` | number | `50000` | Range filter min |
492
+ | `filters[field][max]` | number | `100000` | Range filter max |
493
+ | `filters[field][from]` | string | `2024-01-01` | Date range start |
494
+ | `filters[field][to]` | string | `2024-12-31` | Date range end |
495
+ | `filters[field][]` | string[] | `admin,user` | Multi-select values |
496
+
497
+ #### POST Request — JSON Body
498
+
499
+ ```json
500
+ POST /api/data
501
+ Content-Type: application/json
502
+
503
+ {
504
+ "pageIndex": 2,
505
+ "pageSize": 10,
506
+ "sortBy": "name",
507
+ "sortOrder": "asc",
508
+ "search": "john",
509
+ "searchFields": ["name", "email"],
510
+ "filters": {
511
+ "status": "active",
512
+ "role": ["admin", "user"],
513
+ "salary": { "min": 50000, "max": 100000 },
514
+ "joinDate": { "from": "2024-01-01", "to": "2024-12-31" },
515
+ "isVerified": true
516
+ }
517
+ }
518
+ ```
519
+
520
+ ### Filter Types → API Format
521
+
522
+ | Filter Type | Value Type | GET Example | POST Example |
523
+ |---|---|---|---|
524
+ | `search` | string | `filters[name]=john` | `"name": "john"` |
525
+ | `date` | string | `filters[date]=2024-06-15` | `"date": "2024-06-15"` |
526
+ | `dateRange` | [from, to] | `filters[dob][from]=2024-01-01&filters[dob][to]=2024-12-31` | `"dob": { "from": "2024-01-01", "to": "2024-12-31" }` |
527
+ | `select` | string | `filters[status]=active` | `"status": "active"` |
528
+ | `multiSelect` | string[] | `filters[role][]=admin&filters[role][]=user` | `"role": ["admin", "user"]` |
529
+ | `checkbox` | boolean | `filters[verified]=true` | `"verified": true` |
530
+ | `range` | [min, max] | `filters[salary][min]=50000&filters[salary][max]=100000` | `"salary": { "min": 50000, "max": 100000 }` |
531
+
532
+ ### API Response Format
533
+
534
+ The DataTable expects API responses in this format:
535
+
536
+ ```json
537
+ {
538
+ "status": 200,
539
+ "success": true,
540
+ "message": "success",
541
+ "data": [
542
+ { "id": 1, "name": "User 1", "email": "user1@example.com", "role": "admin" },
543
+ { "id": 2, "name": "User 2", "email": "user2@example.com", "role": "user" }
544
+ ],
545
+ "meta": {
546
+ "pagination": {
547
+ "pageIndex": 2,
548
+ "pageSize": 10,
549
+ "pageCount": 7,
550
+ "totalData": 65
551
+ }
552
+ }
553
+ }
554
+ ```
555
+
556
+ #### Response Fields
557
+
558
+ | Field | Type | Required | Description |
559
+ |---|---|---|---|
560
+ | `status` | number | Yes | HTTP status code |
561
+ | `success` | boolean | Yes | Request success flag |
562
+ | `message` | string | Yes | Response message |
563
+ | `data` | array | Yes | Array of row objects |
564
+ | `meta.pagination.pageIndex` | number | Yes | Current page (must match request) |
565
+ | `meta.pagination.pageSize` | number | Yes | Items per page |
566
+ | `meta.pagination.pageCount` | number | Yes | Total number of pages |
567
+ | `meta.pagination.totalData` | number | Yes | Total number of records (across all pages) |
568
+
569
+ ### Server-Side Behavior Summary
570
+
571
+ | Feature | Props Required | Behavior |
572
+ |---|---|---|
573
+ | Server pagination | `manualPagination` + `totalCount` | Data not sliced client-side; page count from `totalCount` |
574
+ | Server filtering | `manualFiltering` | Client search/filter skipped; filters sent via `onStateChange` |
575
+ | Server sorting | `manualSorting` | Client sort skipped; sort sent via `onStateChange` |
576
+ | Page reset on filter | `manualFiltering` + `manualPagination` | Auto-reset to page 1 when filters/search/sort change |
577
+
578
+ ## Hooks API
579
+
580
+ ### useFilters()
581
+ ```tsx
582
+ const {
583
+ filters, // Current filter array
584
+ addFilter, // Add a filter
585
+ updateFilter, // Update existing filter
586
+ removeFilter, // Remove filter
587
+ clearFilters, // Clear all filters
588
+ clearFieldFilters, // Clear filters for specific fields
589
+ hasActiveFilters, // Check if any filters active
590
+ getFieldFilters, // Get filters for specific field
591
+ } = useFilters();
592
+ ```
593
+
594
+ ### usePagination()
595
+ ```tsx
596
+ const {
597
+ pageIndex, // Current page (1-indexed)
598
+ pageSize, // Items per page
599
+ pageCount, // Total pages
600
+ totalData, // Total items count
601
+ paginationMode, // 'traditional' | 'infinite'
602
+ goToPage, // Jump to specific page
603
+ nextPage, // Go to next page
604
+ previousPage, // Go to previous page
605
+ setPageSize, // Change page size
606
+ updatePaginationMeta, // Update all pagination data
607
+ setPaginationMode, // Switch pagination mode
608
+ canGoNext, // Can go to next?
609
+ canGoPrevious, // Can go to previous?
610
+ } = usePagination();
611
+ ```
612
+
613
+ ### useSelection()
614
+ ```tsx
615
+ const {
616
+ rowSelection, // Selected rows object
617
+ expandedRows, // Expanded rows object
618
+ toggleRow, // Toggle row selection
619
+ toggleAll, // Toggle all rows
620
+ selectRows, // Select multiple rows
621
+ deselectRows, // Deselect multiple rows
622
+ clearSelection, // Clear all selections
623
+ isRowSelected, // Check if row selected
624
+ selectedCount, // Count of selected rows
625
+ toggleExpanded, // Toggle row expansion
626
+ isRowExpanded, // Check if row expanded
627
+ expandedCount, // Count of expanded rows
628
+ getSelectedRowIds, // Get array of selected IDs
629
+ } = useSelection();
630
+ ```
631
+
632
+ ### useSort()
633
+ ```tsx
634
+ const {
635
+ sort, // { column: string | null, order: 'asc' | 'desc' | null }
636
+ setSort, // Set sort column and order
637
+ toggleSort, // Cycle sort: asc -> desc -> none
638
+ clearSort, // Clear sorting
639
+ isSorted, // Check if column sorted
640
+ getSortOrder, // Get sort order for column
641
+ } = useSort();
642
+ ```
643
+
644
+ ### useColumnVisibility()
645
+ ```tsx
646
+ const {
647
+ visibility, // Visibility state object
648
+ toggleColumn, // Toggle column visibility
649
+ showColumn, // Show column
650
+ hideColumn, // Hide column
651
+ isColumnVisible, // Check if visible
652
+ showAllColumns, // Show all columns
653
+ hideAllColumns, // Hide all columns
654
+ setVisibility, // Set all visibility
655
+ } = useColumnVisibility();
656
+ ```
657
+
658
+ ## Filter Types Configuration
659
+
660
+ ### Search Filter
661
+ ```tsx
662
+ {
663
+ id: 'name',
664
+ label: 'Search Name',
665
+ type: 'search',
666
+ placeholder: 'Enter name...',
667
+ }
668
+ ```
669
+
670
+ ### Date Filter
671
+ ```tsx
672
+ {
673
+ id: 'createdAt',
674
+ label: 'Created Date',
675
+ type: 'date',
676
+ }
677
+ ```
678
+
679
+ ### Date Range Filter
680
+ ```tsx
681
+ {
682
+ id: 'dateRange',
683
+ label: 'Date Range',
684
+ type: 'dateRange',
685
+ }
686
+ ```
687
+
688
+ ### Select Filter
689
+ ```tsx
690
+ {
691
+ id: 'status',
692
+ label: 'Status',
693
+ type: 'select',
694
+ options: [
695
+ { label: 'Active', value: 'active' },
696
+ { label: 'Inactive', value: 'inactive' },
697
+ ],
698
+ placeholder: 'Select status...',
699
+ }
700
+ ```
701
+
702
+ ### Multi-Select Filter
703
+ ```tsx
704
+ {
705
+ id: 'roles',
706
+ label: 'Roles',
707
+ type: 'multiSelect',
708
+ options: [
709
+ { label: 'Admin', value: 'admin' },
710
+ { label: 'User', value: 'user' },
711
+ ],
712
+ placeholder: 'Select roles...',
713
+ }
714
+ ```
715
+
716
+ ### Checkbox Filter
717
+ ```tsx
718
+ {
719
+ id: 'isVerified',
720
+ label: 'Is Verified',
721
+ type: 'checkbox',
722
+ }
723
+ ```
724
+
725
+ ### Range Filter
726
+ ```tsx
727
+ {
728
+ id: 'salary',
729
+ label: 'Salary Range',
730
+ type: 'range',
731
+ minValue: 0,
732
+ maxValue: 1000000,
733
+ }
734
+ ```
735
+
736
+ ## DataTable Props
737
+
738
+ ```tsx
739
+ interface DataTableProps<T> {
740
+ // Required
741
+ columns: DataTableColumn<T>[];
742
+ data: T[];
743
+ rowKey: keyof T;
744
+
745
+ // Display
746
+ isLoading?: boolean;
747
+ error?: string | null;
748
+ onRowClick?: (row: T) => void;
749
+ emptyStateMessage?: string;
750
+
751
+ // Selection & Expand
752
+ showSelection?: boolean;
753
+ showExpand?: boolean;
754
+ stickySelection?: boolean;
755
+ stickyExpand?: boolean;
756
+ showRowNumber?: boolean;
757
+ stickyRowNumber?: boolean;
758
+ expandContent?: (row: T) => React.ReactNode;
759
+
760
+ // Pagination
761
+ paginationMode?: 'traditional' | 'infinite';
762
+ pageSize?: number;
763
+ pageSizeOptions?: number[];
764
+ tableHeight?: string | number;
765
+ infiniteScrollHeight?: string | number;
766
+
767
+ // Search & Filter
768
+ showFilters?: boolean;
769
+ showSearch?: boolean;
770
+ searchFields?: string[];
771
+ searchPlaceholder?: string;
772
+
773
+ // Server-side
774
+ manualPagination?: boolean; // Enable server-side pagination
775
+ manualSorting?: boolean; // Enable server-side sorting
776
+ manualFiltering?: boolean; // Enable server-side filtering
777
+ totalCount?: number; // Required when manualPagination=true
778
+ onStateChange?: (state: DataTableState) => void; // Fires on any state change
779
+ }
780
+ ```
781
+
782
+ ## Styling & Customization
783
+
784
+ The DataTable uses Tailwind CSS classes and shadcn/ui components. Customize by:
785
+
786
+ 1. **Modifying component styles** - Edit the component files directly
787
+ 2. **Using Tailwind config** - Customize via `tailwind.config.ts`
788
+ 3. **Custom cell rendering** - Use the `cell` prop in column config
789
+ 4. **Custom header rendering** - Use `header` prop with JSX
790
+
791
+ ## Performance Considerations
792
+
793
+ - **Virtual scrolling**: Consider for tables with 1000+ rows
794
+ - **Pagination**: Use traditional or infinite scroll based on UX needs
795
+ - **Client vs Server filtering**: Choose based on dataset size
796
+ - **Memoization**: Use `React.memo` for expensive cell renders
797
+ - **Key prop**: Always use a stable unique `rowKey`
798
+
799
+ ## Accessibility
800
+
801
+ - Fully keyboard navigable (arrow keys, tab, enter)
802
+ - ARIA labels on all interactive elements
803
+ - Semantic HTML structure
804
+ - Screen reader friendly
805
+ - High contrast support
806
+
807
+ ## Browser Support
808
+
809
+ - Chrome/Edge (latest 2 versions)
810
+ - Firefox (latest 2 versions)
811
+ - Safari (latest 2 versions)
812
+ - Mobile browsers (iOS Safari, Chrome Mobile)
813
+
814
+ ## Demo
815
+
816
+ Visit `/data-table-demo` to see all features in action!