@actabldesign/bellhop-react 0.0.11 → 0.0.12

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.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Auto-generated React wrapper components
3
+ *
4
+ * This file exports all React wrapper components generated by @stencil/react-output-target.
5
+ */
6
+ export * from './components';
@@ -0,0 +1,514 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * BhDataTable - Dynamic Table with TanStack Table
4
+ *
5
+ * A composable data table component that combines Bellhop table primitives
6
+ * with TanStack Table for sorting, filtering, pagination, selection,
7
+ * expansion, grouping, and editing.
8
+ * Uses existing Bellhop components where possible.
9
+ */
10
+ import React, { useState, useMemo, useRef, useEffect, useCallback, } from 'react';
11
+ import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel, getPaginationRowModel, getExpandedRowModel, getGroupedRowModel, flexRender, } from '@tanstack/react-table';
12
+ import { Table, TableWrapper, TableHead, TableBody, TableRow, TableHeaderCell, TableCell, TableEmpty, TableActionBar, TablePagination, } from './index';
13
+ // Import existing Bellhop components
14
+ import { BhButtonIcon, BhInputText } from '../stencil-generated';
15
+ function TableCheckbox({ checked, indeterminate = false, disabled = false, onChange, 'aria-label': ariaLabel, }) {
16
+ const inputRef = useRef(null);
17
+ useEffect(() => {
18
+ if (inputRef.current) {
19
+ inputRef.current.indeterminate = indeterminate;
20
+ }
21
+ }, [indeterminate]);
22
+ return (_jsxs("label", { className: "bh-table-checkbox", children: [_jsx("input", { ref: inputRef, type: "checkbox", checked: checked, disabled: disabled, onChange: onChange, "aria-label": ariaLabel, className: "bh-table-checkbox__input" }), _jsxs("span", { className: `bh-table-checkbox__box ${checked || indeterminate ? 'bh-table-checkbox__box--checked' : ''} ${disabled ? 'bh-table-checkbox__box--disabled' : ''}`, children: [checked && !indeterminate && (_jsx("span", { className: "material-symbols-outlined bh-table-checkbox__icon", children: "check" })), indeterminate && (_jsx("span", { className: "material-symbols-outlined bh-table-checkbox__icon", children: "remove" }))] })] }));
23
+ }
24
+ // =============================================================================
25
+ // Checkbox Column Helper
26
+ // =============================================================================
27
+ export function createSelectColumn() {
28
+ return {
29
+ id: 'select',
30
+ header: ({ table }) => (_jsx(TableCheckbox, { checked: table.getIsAllPageRowsSelected(), indeterminate: table.getIsSomePageRowsSelected(), onChange: () => table.toggleAllPageRowsSelected(), "aria-label": "Select all rows" })),
31
+ cell: ({ row }) => (_jsx(TableCheckbox, { checked: row.getIsSelected(), disabled: !row.getCanSelect(), onChange: () => row.toggleSelected(), "aria-label": "Select row" })),
32
+ size: 40,
33
+ enableSorting: false,
34
+ enableColumnFilter: false,
35
+ enableGrouping: false,
36
+ };
37
+ }
38
+ // =============================================================================
39
+ // Expand Column Helper
40
+ // =============================================================================
41
+ export function createExpandColumn() {
42
+ return {
43
+ id: 'expand',
44
+ header: () => null,
45
+ cell: ({ row }) => row.getCanExpand() ? (_jsx(BhButtonIcon, { iconName: row.getIsExpanded() ? 'expand_more' : 'chevron_right', size: "sm", hierarchy: "quaternary", onBhClick: () => row.toggleExpanded(), "aria-label": row.getIsExpanded() ? 'Collapse row' : 'Expand row' })) : null,
46
+ size: 32,
47
+ enableSorting: false,
48
+ enableColumnFilter: false,
49
+ enableGrouping: false,
50
+ };
51
+ }
52
+ // =============================================================================
53
+ // Edit Actions Column Helper
54
+ // =============================================================================
55
+ export function createEditActionsColumn(onEdit, onSave, onCancel, isEditing) {
56
+ return {
57
+ id: 'edit-actions',
58
+ header: () => null,
59
+ cell: ({ row }) => {
60
+ const rowId = row.id;
61
+ const editing = isEditing(rowId);
62
+ if (editing) {
63
+ return (_jsxs("div", { className: "bh-td--edit-actions", children: [_jsx(BhButtonIcon, { iconName: "check", size: "sm", hierarchy: "quaternary", onBhClick: () => onSave(), "aria-label": "Save changes" }), _jsx(BhButtonIcon, { iconName: "close", size: "sm", hierarchy: "quaternary", onBhClick: () => onCancel(), "aria-label": "Cancel editing" })] }));
64
+ }
65
+ return (_jsx(BhButtonIcon, { iconName: "edit", size: "sm", hierarchy: "quaternary", onBhClick: () => onEdit(rowId), "aria-label": "Edit row" }));
66
+ },
67
+ size: 80,
68
+ enableSorting: false,
69
+ enableColumnFilter: false,
70
+ enableGrouping: false,
71
+ meta: {
72
+ editable: false,
73
+ },
74
+ };
75
+ }
76
+ // =============================================================================
77
+ // DataTable Component
78
+ // =============================================================================
79
+ export function DataTable({ data, columns, size = 'default', variant = 'default', enableRowSelection = false, enableMultiRowSelection = true, enableSorting = true, enableFiltering = false, enablePagination = true, enableExpanding = false, pageSize = 10, pageSizeOptions = [10, 25, 50, 100], stickyHeader = false, rounded = false, hoverable = true, loading = false, emptyTitle = 'No data', emptyDescription = 'There are no records to display.', getRowId, getSubRows, renderExpandedRow, onSelectionChange, onSortingChange, onRowClick, actionBarLeft, actionBarRight, bulkActions, maxHeight, className,
80
+ // Grouping props
81
+ enableGrouping = false, groupBy = [], onGroupingChange,
82
+ // Editing props
83
+ enableEditing = false, editMode = 'cell', showEditActions = true, onEditSave, onEditCancel, onCellChange, }) {
84
+ // ==========================================================================
85
+ // State
86
+ // ==========================================================================
87
+ const [sorting, setSorting] = useState([]);
88
+ const [columnFilters, setColumnFilters] = useState([]);
89
+ const [rowSelection, setRowSelection] = useState({});
90
+ const [expanded, setExpanded] = useState({});
91
+ const [pagination, setPagination] = useState({
92
+ pageIndex: 0,
93
+ pageSize,
94
+ });
95
+ const [globalFilter, setGlobalFilter] = useState('');
96
+ // Grouping state
97
+ const [grouping, setGrouping] = useState(groupBy);
98
+ // Editing state
99
+ const [editState, setEditState] = useState({
100
+ rowId: null,
101
+ columnId: null,
102
+ });
103
+ const [editingValues, setEditingValues] = useState({});
104
+ // Sync groupBy prop with internal state
105
+ useEffect(() => {
106
+ setGrouping(groupBy);
107
+ }, [groupBy]);
108
+ // Track previous rowSelection to detect changes for onSelectionChange callback
109
+ const prevRowSelectionRef = useRef(rowSelection);
110
+ // Compute column visibility to hide grouping columns when grouping is enabled
111
+ const columnVisibility = useMemo(() => {
112
+ if (!enableGrouping || grouping.length === 0) {
113
+ return {};
114
+ }
115
+ // Hide all columns that are being used for grouping
116
+ const visibility = {};
117
+ grouping.forEach((columnId) => {
118
+ visibility[columnId] = false;
119
+ });
120
+ return visibility;
121
+ }, [enableGrouping, grouping]);
122
+ // ==========================================================================
123
+ // Editing Handlers
124
+ // ==========================================================================
125
+ const isEditing = useCallback((rowId, columnId) => {
126
+ if (editState.rowId !== rowId)
127
+ return false;
128
+ if (editMode === 'cell') {
129
+ return columnId ? editState.columnId === columnId : false;
130
+ }
131
+ return true; // Row mode - entire row is editing
132
+ }, [editState, editMode]);
133
+ const isRowEditing = useCallback((rowId) => {
134
+ return editState.rowId === rowId;
135
+ }, [editState.rowId]);
136
+ const startEditing = useCallback((rowId, columnId) => {
137
+ // Initialize editing values from current row
138
+ const initialValues = {};
139
+ columns.forEach((col) => {
140
+ const colId = 'accessorKey' in col
141
+ ? String(col.accessorKey)
142
+ : 'id' in col
143
+ ? col.id
144
+ : '';
145
+ if (colId && col.meta?.editable !== false) {
146
+ // Find row data
147
+ const rowIndex = data.findIndex((_, i) => (getRowId ? getRowId(data[i], i) : String(i)) === rowId);
148
+ if (rowIndex !== -1) {
149
+ const row = data[rowIndex];
150
+ initialValues[colId] = row[colId];
151
+ }
152
+ }
153
+ });
154
+ setEditingValues(initialValues);
155
+ setEditState({
156
+ rowId,
157
+ columnId: editMode === 'cell' ? columnId || null : null,
158
+ });
159
+ }, [columns, data, editMode, getRowId]);
160
+ const cancelEditing = useCallback(() => {
161
+ if (!editState.rowId)
162
+ return;
163
+ const rowIndex = data.findIndex((_, i) => (getRowId ? getRowId(data[i], i) : String(i)) === editState.rowId);
164
+ if (rowIndex !== -1) {
165
+ onEditCancel?.(editState.rowId, data[rowIndex]);
166
+ }
167
+ setEditState({ rowId: null, columnId: null });
168
+ setEditingValues({});
169
+ }, [editState.rowId, data, getRowId, onEditCancel]);
170
+ const saveEditing = useCallback(() => {
171
+ if (!editState.rowId)
172
+ return;
173
+ const rowIndex = data.findIndex((_, i) => (getRowId ? getRowId(data[i], i) : String(i)) === editState.rowId);
174
+ if (rowIndex === -1)
175
+ return;
176
+ const originalRow = data[rowIndex];
177
+ const changes = {};
178
+ // Determine what changed
179
+ Object.entries(editingValues).forEach(([columnId, newValue]) => {
180
+ const originalValue = originalRow[columnId];
181
+ if (originalValue !== newValue) {
182
+ changes[columnId] = newValue;
183
+ }
184
+ });
185
+ if (Object.keys(changes).length > 0) {
186
+ onEditSave?.({
187
+ rowId: editState.rowId,
188
+ rowIndex,
189
+ originalRow,
190
+ changes,
191
+ });
192
+ }
193
+ setEditState({ rowId: null, columnId: null });
194
+ setEditingValues({});
195
+ }, [editState.rowId, editingValues, data, getRowId, onEditSave]);
196
+ const handleEditValueChange = useCallback((columnId, newValue) => {
197
+ const oldValue = editingValues[columnId];
198
+ setEditingValues((prev) => ({
199
+ ...prev,
200
+ [columnId]: newValue,
201
+ }));
202
+ if (editState.rowId) {
203
+ onCellChange?.(editState.rowId, columnId, oldValue, newValue);
204
+ }
205
+ }, [editingValues, editState.rowId, onCellChange]);
206
+ const handleEditKeyDown = useCallback((e, columnId) => {
207
+ if (e.key === 'Enter') {
208
+ e.preventDefault();
209
+ saveEditing();
210
+ }
211
+ else if (e.key === 'Escape') {
212
+ e.preventDefault();
213
+ cancelEditing();
214
+ }
215
+ else if (e.key === 'Tab' && editMode === 'cell') {
216
+ e.preventDefault();
217
+ // Move to next editable cell
218
+ const editableColumns = columns.filter((col) => col.meta?.editable !== false);
219
+ const currentIndex = editableColumns.findIndex((c) => {
220
+ const colId = 'accessorKey' in c
221
+ ? String(c.accessorKey)
222
+ : 'id' in c
223
+ ? c.id
224
+ : '';
225
+ return colId === columnId;
226
+ });
227
+ if (currentIndex !== -1) {
228
+ const nextIndex = e.shiftKey ? currentIndex - 1 : currentIndex + 1;
229
+ if (nextIndex >= 0 && nextIndex < editableColumns.length) {
230
+ const nextCol = editableColumns[nextIndex];
231
+ const nextColId = 'accessorKey' in nextCol
232
+ ? String(nextCol.accessorKey)
233
+ : 'id' in nextCol
234
+ ? nextCol.id
235
+ : '';
236
+ setEditState((prev) => ({
237
+ ...prev,
238
+ columnId: nextColId || null,
239
+ }));
240
+ }
241
+ else {
242
+ saveEditing();
243
+ }
244
+ }
245
+ }
246
+ }, [editMode, columns, saveEditing, cancelEditing]);
247
+ // ==========================================================================
248
+ // Build columns with selection/expansion/editing if enabled
249
+ // ==========================================================================
250
+ const tableColumns = useMemo(() => {
251
+ const cols = [];
252
+ if (enableExpanding && !enableGrouping) {
253
+ cols.push(createExpandColumn());
254
+ }
255
+ if (enableRowSelection) {
256
+ cols.push(createSelectColumn());
257
+ }
258
+ cols.push(...columns);
259
+ if (enableEditing && editMode === 'row' && showEditActions) {
260
+ cols.push(createEditActionsColumn(startEditing, saveEditing, cancelEditing, isRowEditing));
261
+ }
262
+ return cols;
263
+ }, [
264
+ columns,
265
+ enableRowSelection,
266
+ enableExpanding,
267
+ enableGrouping,
268
+ enableEditing,
269
+ editMode,
270
+ showEditActions,
271
+ startEditing,
272
+ saveEditing,
273
+ cancelEditing,
274
+ isRowEditing,
275
+ ]);
276
+ // ==========================================================================
277
+ // Create table instance
278
+ // ==========================================================================
279
+ const table = useReactTable({
280
+ data,
281
+ columns: tableColumns,
282
+ state: {
283
+ sorting,
284
+ columnFilters,
285
+ rowSelection,
286
+ expanded,
287
+ pagination,
288
+ globalFilter,
289
+ grouping,
290
+ columnVisibility,
291
+ },
292
+ getRowId,
293
+ getSubRows,
294
+ getRowCanExpand: enableExpanding && renderExpandedRow ? () => true : undefined,
295
+ enableRowSelection: enableRowSelection
296
+ ? enableMultiRowSelection
297
+ ? true
298
+ : (row) => !Object.keys(rowSelection).length || rowSelection[row.id]
299
+ : false,
300
+ enableMultiRowSelection,
301
+ enableSorting,
302
+ enableFilters: enableFiltering,
303
+ enableExpanding: enableExpanding || enableGrouping,
304
+ enableGrouping,
305
+ // Prevent TanStack Table from auto-resetting expanded state when data/columns change
306
+ // This fixes the issue where grouped rows collapse when selecting items
307
+ autoResetExpanded: false,
308
+ onSortingChange: (updater) => {
309
+ const newSorting = typeof updater === 'function' ? updater(sorting) : updater;
310
+ setSorting(newSorting);
311
+ onSortingChange?.(newSorting);
312
+ },
313
+ onColumnFiltersChange: setColumnFilters,
314
+ onRowSelectionChange: (updater) => {
315
+ const newSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
316
+ setRowSelection(newSelection);
317
+ },
318
+ onExpandedChange: setExpanded,
319
+ onPaginationChange: setPagination,
320
+ onGlobalFilterChange: setGlobalFilter,
321
+ onGroupingChange: (updater) => {
322
+ const newGrouping = typeof updater === 'function' ? updater(grouping) : updater;
323
+ setGrouping(newGrouping);
324
+ onGroupingChange?.(newGrouping);
325
+ },
326
+ getCoreRowModel: getCoreRowModel(),
327
+ getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
328
+ getFilteredRowModel: enableFiltering ? getFilteredRowModel() : undefined,
329
+ // Disable pagination when grouping is enabled
330
+ getPaginationRowModel: enablePagination && !enableGrouping ? getPaginationRowModel() : undefined,
331
+ getExpandedRowModel: enableExpanding || enableGrouping ? getExpandedRowModel() : undefined,
332
+ getGroupedRowModel: enableGrouping ? getGroupedRowModel() : undefined,
333
+ });
334
+ // Handle selection change callback after table is stable (via useEffect)
335
+ // This prevents issues with grouped rows collapsing when selecting items
336
+ // IMPORTANT: We use a ref for the table to avoid the effect re-running when table changes
337
+ // because table is recreated on every render, which would cause cascading state updates
338
+ const tableRef = useRef(table);
339
+ tableRef.current = table;
340
+ useEffect(() => {
341
+ if (!onSelectionChange)
342
+ return;
343
+ if (prevRowSelectionRef.current === rowSelection)
344
+ return;
345
+ prevRowSelectionRef.current = rowSelection;
346
+ // Use tableRef.current to get the latest table without adding it as a dependency
347
+ const currentTable = tableRef.current;
348
+ const selectedRows = Object.keys(rowSelection)
349
+ .filter((key) => rowSelection[key])
350
+ .map((key) => {
351
+ const row = currentTable.getRow(key);
352
+ return row?.original;
353
+ })
354
+ .filter(Boolean);
355
+ onSelectionChange(selectedRows);
356
+ }, [rowSelection, onSelectionChange]);
357
+ // ==========================================================================
358
+ // Render Helpers
359
+ // ==========================================================================
360
+ const renderEditInput = (column, cell) => {
361
+ const columnId = 'accessorKey' in column.columnDef
362
+ ? String(column.columnDef.accessorKey)
363
+ : column.id;
364
+ const currentValue = editingValues[columnId] ?? cell.getValue();
365
+ const editType = column.columnDef.meta?.editType || 'text';
366
+ const editOptions = column.columnDef.meta?.editOptions;
367
+ // Select type
368
+ if (editType === 'select' && editOptions) {
369
+ return (_jsx("select", { className: "bh-edit-select", value: String(currentValue ?? ''), onChange: (e) => handleEditValueChange(columnId, e.target.value), onKeyDown: (e) => handleEditKeyDown(e, columnId), autoFocus: true, children: editOptions.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value))) }));
370
+ }
371
+ // Text/Number/Date input
372
+ return (_jsx("input", { type: editType === 'number' ? 'number' : editType === 'date' ? 'date' : 'text', className: "bh-edit-input", value: currentValue != null ? String(currentValue) : '', onChange: (e) => {
373
+ const newValue = editType === 'number' ? Number(e.target.value) : e.target.value;
374
+ handleEditValueChange(columnId, newValue);
375
+ }, onKeyDown: (e) => handleEditKeyDown(e, columnId), autoFocus: true }));
376
+ };
377
+ const renderGroupRow = (row) => {
378
+ const groupingColumnId = row.groupingColumnId;
379
+ const groupValue = row.groupingValue;
380
+ const isExpanded = row.getIsExpanded();
381
+ const leafRows = row.getLeafRows();
382
+ const depth = row.depth;
383
+ return (_jsx(TableRow, { className: "bh-tr--group", onClick: () => row.toggleExpanded(), children: _jsx(TableCell, { colSpan: tableColumns.length, className: "bh-td--group-label", children: _jsxs("div", { className: "bh-group-content", style: { paddingLeft: `${depth * 24}px` }, children: [_jsx(BhButtonIcon, { iconName: isExpanded ? 'expand_more' : 'chevron_right', size: "sm", hierarchy: "quaternary", "aria-label": isExpanded ? 'Collapse group' : 'Expand group' }), _jsxs("span", { className: "bh-group-label__text", children: [_jsxs("span", { className: "bh-group-label__column", children: [groupingColumnId, ": "] }), _jsx("span", { className: "bh-group-label__value", children: String(groupValue) })] }), _jsxs("span", { className: "bh-group-label__count", children: ["(", leafRows.length, " ", leafRows.length === 1 ? 'item' : 'items', ")"] })] }) }) }, row.id));
384
+ };
385
+ const renderDataRow = (row) => {
386
+ const rowId = row.id;
387
+ const rowIsEditing = isRowEditing(rowId);
388
+ return (_jsxs(React.Fragment, { children: [_jsx(TableRow, { selected: row.getIsSelected(), clickable: !!onRowClick && !rowIsEditing, expandable: row.getCanExpand(), expanded: row.getIsExpanded(), className: rowIsEditing ? 'bh-tr--editing' : undefined, onClick: onRowClick && !rowIsEditing
389
+ ? () => onRowClick(row.original)
390
+ : undefined, children: row.getVisibleCells().map((cell) => {
391
+ const column = cell.column;
392
+ const isCheckbox = column.id === 'select';
393
+ const isExpand = column.id === 'expand';
394
+ const isEditActions = column.id === 'edit-actions';
395
+ const columnId = 'accessorKey' in column.columnDef
396
+ ? String(column.columnDef.accessorKey)
397
+ : column.id;
398
+ const canEdit = enableEditing && column.columnDef.meta?.editable !== false;
399
+ const isCellEditing = isEditing(rowId, columnId);
400
+ // For grouped cells, skip rendering aggregated/placeholder cells
401
+ if (cell.getIsGrouped()) {
402
+ return null;
403
+ }
404
+ if (cell.getIsAggregated()) {
405
+ return (_jsx(TableCell, { children: flexRender(column.columnDef.aggregatedCell ?? column.columnDef.cell, cell.getContext()) }, cell.id));
406
+ }
407
+ if (cell.getIsPlaceholder()) {
408
+ return _jsx(TableCell, {}, cell.id);
409
+ }
410
+ return (_jsx(TableCell, { checkbox: isCheckbox, expandTrigger: isExpand, shrink: isCheckbox || isExpand || isEditActions, className: canEdit && !isCellEditing && !isCheckbox && !isExpand && !isEditActions
411
+ ? 'bh-td--editable'
412
+ : isCellEditing
413
+ ? 'bh-td--editing'
414
+ : undefined, onClick: isCheckbox || isExpand || isEditActions
415
+ ? (e) => e.stopPropagation()
416
+ : undefined, onDoubleClick: canEdit &&
417
+ editMode === 'cell' &&
418
+ !isCellEditing &&
419
+ !isCheckbox &&
420
+ !isExpand &&
421
+ !isEditActions
422
+ ? () => startEditing(rowId, columnId)
423
+ : undefined, style: {
424
+ width: cell.column.getSize() !== 150
425
+ ? cell.column.getSize()
426
+ : undefined,
427
+ }, children: isCellEditing || (rowIsEditing && canEdit && editMode === 'row') ? (renderEditInput(column, cell)) : (flexRender(column.columnDef.cell, cell.getContext())) }, cell.id));
428
+ }) }), row.getIsExpanded() && renderExpandedRow && !row.getIsGrouped() && (_jsx(TableRow, { expansion: true, children: _jsx(TableCell, { expansion: true, colSpan: tableColumns.length, children: renderExpandedRow(row) }) }))] }, row.id));
429
+ };
430
+ // ==========================================================================
431
+ // Render
432
+ // ==========================================================================
433
+ const selectedCount = Object.keys(rowSelection).filter((key) => rowSelection[key]).length;
434
+ const showActionBar = actionBarLeft || actionBarRight || (selectedCount > 0 && bulkActions);
435
+ return (_jsxs("div", { className: className, children: [showActionBar && (_jsx(TableActionBar, { selectionCount: selectedCount, left: selectedCount > 0 ? bulkActions : actionBarLeft, right: actionBarRight })), _jsx(TableWrapper, { maxHeight: maxHeight, scrollY: !!maxHeight, children: _jsxs(Table, { size: size, variant: variant, rounded: rounded, hoverable: hoverable, loading: loading, children: [_jsxs(TableHead, { sticky: stickyHeader, children: [table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { children: headerGroup.headers.map((header) => {
436
+ const isSortable = header.column.getCanSort();
437
+ const sortDirection = header.column.getIsSorted();
438
+ const isCheckbox = header.id === 'select';
439
+ const isExpand = header.id === 'expand';
440
+ const isEditActions = header.id === 'edit-actions';
441
+ return (_jsx(TableHeaderCell, { sortable: isSortable, sortDirection: sortDirection || null, onClick: isSortable
442
+ ? header.column.getToggleSortingHandler()
443
+ : undefined, checkbox: isCheckbox, shrink: isCheckbox || isExpand || isEditActions, style: {
444
+ width: header.getSize() !== 150
445
+ ? header.getSize()
446
+ : undefined,
447
+ }, children: header.isPlaceholder
448
+ ? null
449
+ : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
450
+ }) }, headerGroup.id))), enableFiltering && (_jsx(TableRow, { filter: true, children: table.getHeaderGroups()[0]?.headers.map((header) => {
451
+ const column = header.column;
452
+ const canFilter = column.getCanFilter();
453
+ const isCheckbox = header.id === 'select';
454
+ const isExpand = header.id === 'expand';
455
+ const isEditActions = header.id === 'edit-actions';
456
+ if (!canFilter || isCheckbox || isExpand || isEditActions) {
457
+ return _jsx(TableCell, { filter: true }, header.id);
458
+ }
459
+ return (_jsx(TableCell, { filter: true, children: _jsx(BhInputText, { placeholder: "", label: "", value: column.getFilterValue() ?? '', onBhInput: (e) => column.setFilterValue(e.detail) }) }, header.id));
460
+ }) }))] }), _jsx(TableBody, { children: table.getRowModel().rows.length === 0 ? (_jsx(TableEmpty, { title: emptyTitle, description: emptyDescription, colSpan: tableColumns.length, icon: _jsx("span", { className: "material-symbols-outlined", children: "inbox" }) })) : (table.getRowModel().rows.map((row) => {
461
+ // Check if this is a group row
462
+ if (row.getIsGrouped()) {
463
+ return renderGroupRow(row);
464
+ }
465
+ // Regular data row
466
+ return renderDataRow(row);
467
+ })) })] }) }), enablePagination && !enableGrouping && data.length > 0 && (_jsx(TablePagination, { page: table.getState().pagination.pageIndex + 1, totalPages: table.getPageCount(), totalItems: data.length, pageSize: table.getState().pagination.pageSize, pageSizeOptions: pageSizeOptions, onPageChange: (page) => table.setPageIndex(page - 1), onPageSizeChange: (newSize) => table.setPageSize(newSize) }))] }));
468
+ }
469
+ export function useDataTable({ data, columns, enableSorting = true, enableFiltering = false, enablePagination = true, enableRowSelection = false, enableExpanding = false, enableGrouping = false, groupBy = [], pageSize = 10, getRowId, getSubRows, }) {
470
+ const [sorting, setSorting] = useState([]);
471
+ const [columnFilters, setColumnFilters] = useState([]);
472
+ const [rowSelection, setRowSelection] = useState({});
473
+ const [expanded, setExpanded] = useState({});
474
+ const [pagination, setPagination] = useState({
475
+ pageIndex: 0,
476
+ pageSize,
477
+ });
478
+ const [grouping, setGrouping] = useState(groupBy);
479
+ return useReactTable({
480
+ data,
481
+ columns,
482
+ state: {
483
+ sorting,
484
+ columnFilters,
485
+ rowSelection,
486
+ expanded,
487
+ pagination,
488
+ grouping,
489
+ },
490
+ getRowId,
491
+ getSubRows,
492
+ enableRowSelection,
493
+ enableSorting,
494
+ enableFilters: enableFiltering,
495
+ enableExpanding: enableExpanding || enableGrouping,
496
+ enableGrouping,
497
+ onSortingChange: setSorting,
498
+ onColumnFiltersChange: setColumnFilters,
499
+ onRowSelectionChange: setRowSelection,
500
+ onExpandedChange: setExpanded,
501
+ onPaginationChange: setPagination,
502
+ onGroupingChange: setGrouping,
503
+ getCoreRowModel: getCoreRowModel(),
504
+ getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
505
+ getFilteredRowModel: enableFiltering ? getFilteredRowModel() : undefined,
506
+ getPaginationRowModel: enablePagination && !enableGrouping ? getPaginationRowModel() : undefined,
507
+ getExpandedRowModel: enableExpanding || enableGrouping ? getExpandedRowModel() : undefined,
508
+ getGroupedRowModel: enableGrouping ? getGroupedRowModel() : undefined,
509
+ });
510
+ }
511
+ // =============================================================================
512
+ // Exports
513
+ // =============================================================================
514
+ export { DataTable as BhDataTable };
@@ -0,0 +1,95 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Bellhop Table Primitives
4
+ *
5
+ * Composable table components for building static and dynamic tables.
6
+ * These primitives can be used standalone or combined with TanStack Table
7
+ * for advanced features like sorting, filtering, and pagination.
8
+ * Uses existing Bellhop components for consistency.
9
+ */
10
+ import { forwardRef, createContext, useContext } from 'react';
11
+ import { BhButtonIcon } from '../stencil-generated';
12
+ // =============================================================================
13
+ // Context
14
+ // =============================================================================
15
+ const TableContext = createContext({
16
+ size: 'default',
17
+ variant: 'default',
18
+ hoverable: true,
19
+ });
20
+ const useTableContext = () => useContext(TableContext);
21
+ // =============================================================================
22
+ // Utility Functions
23
+ // =============================================================================
24
+ function clsx(...classes) {
25
+ return classes.filter(Boolean).join(' ');
26
+ }
27
+ export const TableWrapper = forwardRef(({ className, children, maxHeight, maxWidth, scrollX, scrollY, style, ...props }, ref) => {
28
+ return (_jsx("div", { ref: ref, className: clsx('bh-table-wrapper', scrollX && 'bh-table-wrapper--scroll-x', scrollY && 'bh-table-wrapper--scroll-y', className), style: {
29
+ maxHeight: maxHeight,
30
+ maxWidth: maxWidth,
31
+ ...style,
32
+ }, tabIndex: 0, ...props, children: children }));
33
+ });
34
+ TableWrapper.displayName = 'TableWrapper';
35
+ export const Table = forwardRef(({ className, children, size = 'default', variant = 'default', rounded = false, hoverable = true, responsive = false, loading = false, ...props }, ref) => {
36
+ return (_jsx(TableContext.Provider, { value: { size, variant, hoverable }, children: _jsx("table", { ref: ref, className: clsx('bh-table', `bh-table--${size}`, variant === 'bordered' && 'bh-table--bordered', variant === 'striped' && 'bh-table--striped', rounded && 'bh-table--rounded', responsive && 'bh-table--responsive', loading && 'bh-table--loading', className), ...props, children: children }) }));
37
+ });
38
+ Table.displayName = 'Table';
39
+ export const TableCaption = forwardRef(({ className, children, position = 'top', srOnly = false, ...props }, ref) => {
40
+ return (_jsx("caption", { ref: ref, className: clsx('bh-caption', position === 'bottom' && 'bh-caption--bottom', srOnly && 'bh-caption--sr-only', className), ...props, children: children }));
41
+ });
42
+ TableCaption.displayName = 'TableCaption';
43
+ export const TableHead = forwardRef(({ className, children, sticky = false, subtle = false, ...props }, ref) => {
44
+ return (_jsx("thead", { ref: ref, className: clsx('bh-thead', sticky && 'bh-thead--sticky', subtle && 'bh-thead--subtle', className), ...props, children: children }));
45
+ });
46
+ TableHead.displayName = 'TableHead';
47
+ export const TableBody = forwardRef(({ className, children, ...props }, ref) => {
48
+ return (_jsx("tbody", { ref: ref, className: clsx('bh-tbody', className), ...props, children: children }));
49
+ });
50
+ TableBody.displayName = 'TableBody';
51
+ export const TableFooter = forwardRef(({ className, children, ...props }, ref) => {
52
+ return (_jsx("tfoot", { ref: ref, className: clsx('bh-tfoot', className), ...props, children: children }));
53
+ });
54
+ TableFooter.displayName = 'TableFooter';
55
+ export const TableRow = forwardRef(({ className, children, selected = false, clickable = false, expandable = false, expanded = false, filter = false, expansion = false, ...props }, ref) => {
56
+ const { hoverable } = useTableContext();
57
+ return (_jsx("tr", { ref: ref, className: clsx('bh-tr', hoverable && 'bh-tr--hoverable', selected && 'bh-tr--selected', clickable && 'bh-tr--clickable', expandable && 'bh-tr--expandable', expanded && 'bh-tr--expanded', filter && 'bh-tr--filter', expansion && 'bh-tr--expansion', className), ...props, children: children }));
58
+ });
59
+ TableRow.displayName = 'TableRow';
60
+ export const TableHeaderCell = forwardRef(({ className, children, align = 'left', sortable = false, sortDirection = null, shrink = false, expand = false, checkbox = false, actions = false, pinnedLeft = false, pinnedRight = false, onClick, ...props }, ref) => {
61
+ const isSorted = sortDirection !== null;
62
+ return (_jsx("th", { ref: ref, className: clsx('bh-th', `bh-th--${align}`, sortable && 'bh-th--sortable', isSorted && 'bh-th--sorted', shrink && 'bh-th--shrink', expand && 'bh-th--expand', checkbox && 'bh-th--checkbox', actions && 'bh-th--actions', pinnedLeft && 'bh-th--pinned-left', pinnedRight && 'bh-th--pinned-right', className), onClick: sortable ? onClick : undefined, "aria-sort": sortDirection === 'asc'
63
+ ? 'ascending'
64
+ : sortDirection === 'desc'
65
+ ? 'descending'
66
+ : undefined, ...props, children: _jsxs("span", { className: "bh-th__content", children: [children, sortable && isSorted && (_jsx("span", { className: "bh-th__sort-icon material-symbols-outlined", children: sortDirection === 'asc' ? 'arrow_upward' : 'arrow_downward' }))] }) }));
67
+ });
68
+ TableHeaderCell.displayName = 'TableHeaderCell';
69
+ export const TableCell = forwardRef(({ className, children, align = 'left', numeric = false, truncate = false, wrap = false, shrink = false, expand = false, checkbox = false, actions = false, expandTrigger = false, filter = false, expansion = false, pinnedLeft = false, pinnedRight = false, dataLabel, ...props }, ref) => {
70
+ return (_jsx("td", { ref: ref, className: clsx('bh-td', `bh-td--${align}`, numeric && 'bh-td--numeric', truncate && 'bh-td--truncate', wrap && 'bh-td--wrap', shrink && 'bh-td--shrink', expand && 'bh-td--expand', checkbox && 'bh-td--checkbox', actions && 'bh-td--actions', expandTrigger && 'bh-td--expand-trigger', filter && 'bh-td--filter', expansion && 'bh-td--expansion', pinnedLeft && 'bh-td--pinned-left', pinnedRight && 'bh-td--pinned-right', className), "data-label": dataLabel, ...props, children: children }));
71
+ });
72
+ TableCell.displayName = 'TableCell';
73
+ export const TableEmpty = forwardRef(({ className, children, icon, title, description, colSpan = 1, ...props }, ref) => {
74
+ return (_jsx("tr", { ref: ref, children: _jsx("td", { colSpan: colSpan, children: _jsxs("div", { className: clsx('bh-table-empty', className), ...props, children: [icon && _jsx("div", { className: "bh-table-empty__icon", children: icon }), title && _jsx("div", { className: "bh-table-empty__title", children: title }), description && (_jsx("div", { className: "bh-table-empty__description", children: description })), children] }) }) }));
75
+ });
76
+ TableEmpty.displayName = 'TableEmpty';
77
+ export const TableActionBar = forwardRef(({ className, children, selectionCount, left, right, ...props }, ref) => {
78
+ const hasSelection = selectionCount !== undefined && selectionCount > 0;
79
+ return (_jsxs("div", { ref: ref, className: clsx('bh-table-action-bar', hasSelection && 'bh-table-action-bar--selected', className), ...props, children: [_jsxs("div", { className: "bh-table-action-bar__left", children: [hasSelection && (_jsxs("span", { className: "bh-table-action-bar__selection-count", children: [selectionCount, " selected"] })), left] }), _jsx("div", { className: "bh-table-action-bar__right", children: right }), children] }));
80
+ });
81
+ TableActionBar.displayName = 'TableActionBar';
82
+ export const TablePagination = forwardRef(({ className, page, totalPages, totalItems, pageSize, pageSizeOptions = [10, 25, 50, 100], onPageChange, onPageSizeChange, ...props }, ref) => {
83
+ const startItem = (page - 1) * pageSize + 1;
84
+ const endItem = Math.min(page * pageSize, totalItems || page * pageSize);
85
+ return (_jsxs("div", { ref: ref, className: clsx('bh-table-pagination', className), ...props, children: [_jsx("div", { className: "bh-table-pagination__info", children: totalItems !== undefined ? (_jsxs(_Fragment, { children: ["Showing ", startItem, " to ", endItem, " of ", totalItems, " results"] })) : (_jsxs(_Fragment, { children: ["Page ", page, " of ", totalPages] })) }), _jsxs("div", { className: "bh-table-pagination__controls", children: [onPageSizeChange && (_jsxs("div", { className: "bh-table-pagination__page-size", children: [_jsx("span", { children: "Rows per page:" }), _jsx("select", { value: pageSize, onChange: (e) => onPageSizeChange(Number(e.target.value)), className: "bh-table-pagination__select", children: pageSizeOptions.map((size) => (_jsx("option", { value: size, children: size }, size))) })] })), _jsx(BhButtonIcon, { iconName: "first_page", size: "sm", hierarchy: "quaternary", disabled: page <= 1, onBhClick: () => onPageChange(1), "aria-label": "First page" }), _jsx(BhButtonIcon, { iconName: "chevron_left", size: "sm", hierarchy: "quaternary", disabled: page <= 1, onBhClick: () => onPageChange(page - 1), "aria-label": "Previous page" }), _jsx(BhButtonIcon, { iconName: "chevron_right", size: "sm", hierarchy: "quaternary", disabled: page >= totalPages, onBhClick: () => onPageChange(page + 1), "aria-label": "Next page" }), _jsx(BhButtonIcon, { iconName: "last_page", size: "sm", hierarchy: "quaternary", disabled: page >= totalPages, onBhClick: () => onPageChange(totalPages), "aria-label": "Last page" })] })] }));
86
+ });
87
+ TablePagination.displayName = 'TablePagination';
88
+ export const ExpandIcon = forwardRef(({ className, expanded = false, ...props }, ref) => {
89
+ return (_jsx("span", { ref: ref, className: clsx('bh-expand-icon', className), ...props, children: _jsx("span", { className: "material-symbols-outlined", children: "chevron_right" }) }));
90
+ });
91
+ ExpandIcon.displayName = 'ExpandIcon';
92
+ // =============================================================================
93
+ // Exports
94
+ // =============================================================================
95
+ export { Table as BhTable, TableWrapper as BhTableWrapper, TableCaption as BhTableCaption, TableHead as BhThead, TableBody as BhTbody, TableFooter as BhTfoot, TableRow as BhTr, TableHeaderCell as BhTh, TableCell as BhTd, TableEmpty as BhTableEmpty, TableActionBar as BhTableActionBar, TablePagination as BhTablePagination, ExpandIcon as BhExpandIcon, };