@dashforge/tw 0.5.0-beta → 0.6.0-beta

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +268 -0
  2. package/dist/index.esm.js +1553 -4
  3. package/dist/src/components/Table/Table.d.ts +24 -0
  4. package/dist/src/components/Table/Table.d.ts.map +1 -0
  5. package/dist/src/components/Table/_internal/getNestedValue.d.ts +20 -0
  6. package/dist/src/components/Table/_internal/getNestedValue.d.ts.map +1 -0
  7. package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts +45 -0
  8. package/dist/src/components/Table/_internal/useColumnAutoDetect.d.ts.map +1 -0
  9. package/dist/src/components/Table/_internal/useControllableState.d.ts +23 -0
  10. package/dist/src/components/Table/_internal/useControllableState.d.ts.map +1 -0
  11. package/dist/src/components/Table/_internal/useDebouncedValue.d.ts +10 -0
  12. package/dist/src/components/Table/_internal/useDebouncedValue.d.ts.map +1 -0
  13. package/dist/src/components/Table/_internal/useTableFilter.d.ts +13 -0
  14. package/dist/src/components/Table/_internal/useTableFilter.d.ts.map +1 -0
  15. package/dist/src/components/Table/_internal/useTableSearch.d.ts +23 -0
  16. package/dist/src/components/Table/_internal/useTableSearch.d.ts.map +1 -0
  17. package/dist/src/components/Table/_internal/useTableSelection.d.ts +25 -0
  18. package/dist/src/components/Table/_internal/useTableSelection.d.ts.map +1 -0
  19. package/dist/src/components/Table/_internal/useTableSort.d.ts +20 -0
  20. package/dist/src/components/Table/_internal/useTableSort.d.ts.map +1 -0
  21. package/dist/src/components/Table/cells/RenderButton.d.ts +13 -0
  22. package/dist/src/components/Table/cells/RenderButton.d.ts.map +1 -0
  23. package/dist/src/components/Table/cells/RenderChip.d.ts +80 -0
  24. package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -0
  25. package/dist/src/components/Table/cells/RenderText.d.ts +21 -0
  26. package/dist/src/components/Table/cells/RenderText.d.ts.map +1 -0
  27. package/dist/src/components/Table/cells/RenderTwoLine.d.ts +20 -0
  28. package/dist/src/components/Table/cells/RenderTwoLine.d.ts.map +1 -0
  29. package/dist/src/components/Table/cells/RowActionsMenu.d.ts +36 -0
  30. package/dist/src/components/Table/cells/RowActionsMenu.d.ts.map +1 -0
  31. package/dist/src/components/Table/cells/index.d.ts +11 -0
  32. package/dist/src/components/Table/cells/index.d.ts.map +1 -0
  33. package/dist/src/components/Table/table.types.d.ts +304 -0
  34. package/dist/src/components/Table/table.types.d.ts.map +1 -0
  35. package/dist/src/components/Table/table.variants.d.ts +285 -0
  36. package/dist/src/components/Table/table.variants.d.ts.map +1 -0
  37. package/dist/src/index.d.ts +7 -1
  38. package/dist/src/index.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/Table/Table.test.tsx +603 -0
  41. package/src/components/Table/Table.tsx +816 -0
  42. package/src/components/Table/_internal/getNestedValue.test.ts +50 -0
  43. package/src/components/Table/_internal/getNestedValue.ts +32 -0
  44. package/src/components/Table/_internal/themeIdentity.test.ts +84 -0
  45. package/src/components/Table/_internal/useColumnAutoDetect.test.ts +123 -0
  46. package/src/components/Table/_internal/useColumnAutoDetect.ts +100 -0
  47. package/src/components/Table/_internal/useControllableState.ts +48 -0
  48. package/src/components/Table/_internal/useDebouncedValue.ts +24 -0
  49. package/src/components/Table/_internal/useTableFilter.ts +37 -0
  50. package/src/components/Table/_internal/useTableSearch.test.ts +115 -0
  51. package/src/components/Table/_internal/useTableSearch.ts +66 -0
  52. package/src/components/Table/_internal/useTableSelection.test.ts +100 -0
  53. package/src/components/Table/_internal/useTableSelection.ts +100 -0
  54. package/src/components/Table/_internal/useTableSort.test.ts +150 -0
  55. package/src/components/Table/_internal/useTableSort.ts +141 -0
  56. package/src/components/Table/cells/RenderButton.tsx +23 -0
  57. package/src/components/Table/cells/RenderChip.tsx +103 -0
  58. package/src/components/Table/cells/RenderText.tsx +37 -0
  59. package/src/components/Table/cells/RenderTwoLine.tsx +34 -0
  60. package/src/components/Table/cells/RowActionsMenu.tsx +147 -0
  61. package/src/components/Table/cells/index.ts +14 -0
  62. package/src/components/Table/table.types.ts +337 -0
  63. package/src/components/Table/table.variants.ts +207 -0
  64. package/src/index.ts +43 -1
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cn } from '../../../utils/cn.js';
3
+
4
+ export interface RenderTwoLineProps {
5
+ primary: ReactNode;
6
+ secondary: ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * Two-line cell renderer: a bold primary line stacked above a muted
12
+ * secondary line. Useful for `Name + email`, `Title + ID`, etc.
13
+ *
14
+ * Replaces the MUI-side `RenderTwoLineCell` without the magic-number
15
+ * vertical-align fudges (`mt: 0.8 / 0.2`) — flexbox handles the
16
+ * vertical layout cleanly.
17
+ *
18
+ * Dashforge identity rule: text colors auto-invert via the
19
+ * `dashforgePreset()` CSS-variable swap. No `dark:` variants needed
20
+ * on neutral palette.
21
+ */
22
+ export function RenderTwoLine(props: RenderTwoLineProps) {
23
+ const { primary, secondary, className } = props;
24
+ return (
25
+ <div className={cn('flex flex-col leading-tight', className)}>
26
+ <span className="font-medium text-neutral-900">
27
+ {primary}
28
+ </span>
29
+ <span className="text-xs text-neutral-500 mt-0.5">
30
+ {secondary}
31
+ </span>
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,147 @@
1
+ import { useState, type ReactNode } from 'react';
2
+ import type { AccessRequirement } from '@dashforge/rbac';
3
+ import { Popover } from '../../Popover/Popover.js';
4
+ import { useAccessState } from '../../../hooks/useAccessState.js';
5
+ import { cn } from '../../../utils/cn.js';
6
+
7
+ /**
8
+ * Single action definition for `<RowActionsMenu>`.
9
+ *
10
+ * `access` (optional) gates the action via RBAC:
11
+ * - `onUnauthorized: 'hide'` → action not rendered
12
+ * - `onUnauthorized: 'disable'` → action visible, disabled
13
+ * - `onUnauthorized: 'readonly'` → action visible, disabled
14
+ */
15
+ export interface TableRowAction<T> {
16
+ label: ReactNode;
17
+ onClick: (row: T) => void;
18
+ icon?: ReactNode;
19
+ color?: 'default' | 'danger' | 'warning';
20
+ disabled?: boolean;
21
+ access?: AccessRequirement;
22
+ }
23
+
24
+ export interface RowActionsMenuProps<T> {
25
+ row: T;
26
+ actions: TableRowAction<T>[];
27
+ /** Custom trigger element. Defaults to a ⋮ icon button. */
28
+ trigger?: ReactNode;
29
+ /** Aria-label for the default trigger. */
30
+ ariaLabel?: string;
31
+ }
32
+
33
+ /**
34
+ * 3-dot row actions menu. Renders a trigger button that opens a
35
+ * Popover (Sprint 3 component) with a list of action menu items.
36
+ *
37
+ * Replaces the MUI-side `RowActions.tsx` (IconButton + Menu) with
38
+ * the TW equivalents. Per-action RBAC honored: hidden actions are
39
+ * filtered out, disabled actions render disabled.
40
+ */
41
+ export function RowActionsMenu<T>(props: RowActionsMenuProps<T>) {
42
+ const { row, actions, trigger, ariaLabel = 'Row actions' } = props;
43
+ const [open, setOpen] = useState(false);
44
+
45
+ // Filter actions by RBAC (hidden actions removed entirely)
46
+ const visibleActions = actions.filter((action) => {
47
+ if (!action.access) return true;
48
+ return action.access.onUnauthorized !== 'hide';
49
+ });
50
+
51
+ if (visibleActions.length === 0) return null;
52
+
53
+ return (
54
+ <Popover
55
+ open={open}
56
+ onOpenChange={setOpen}
57
+ side="bottom"
58
+ align="end"
59
+ content={
60
+ <ul className="flex flex-col gap-0.5 min-w-[140px]" role="menu">
61
+ {visibleActions.map((action, idx) => (
62
+ <RowActionMenuItem
63
+ key={idx}
64
+ action={action}
65
+ row={row}
66
+ onClose={() => setOpen(false)}
67
+ />
68
+ ))}
69
+ </ul>
70
+ }
71
+ >
72
+ {trigger ?? (
73
+ <button
74
+ type="button"
75
+ aria-label={ariaLabel}
76
+ aria-haspopup="menu"
77
+ aria-expanded={open}
78
+ className={cn(
79
+ 'inline-flex items-center justify-center h-8 w-8 rounded-md',
80
+ // Neutral palette auto-inverts via the dashforgePreset()
81
+ // CSS-var swap — no `dark:` variant needed.
82
+ 'text-neutral-500 hover:text-neutral-900',
83
+ 'hover:bg-neutral-100',
84
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
85
+ 'transition-colors motion-reduce:transition-none',
86
+ )}
87
+ >
88
+ <svg
89
+ width="1em"
90
+ height="1em"
91
+ viewBox="0 0 16 16"
92
+ fill="currentColor"
93
+ aria-hidden="true"
94
+ >
95
+ <circle cx="8" cy="3" r="1.4" />
96
+ <circle cx="8" cy="8" r="1.4" />
97
+ <circle cx="8" cy="13" r="1.4" />
98
+ </svg>
99
+ </button>
100
+ )}
101
+ </Popover>
102
+ );
103
+ }
104
+
105
+ function RowActionMenuItem<T>(props: {
106
+ action: TableRowAction<T>;
107
+ row: T;
108
+ onClose: () => void;
109
+ }) {
110
+ const { action, row, onClose } = props;
111
+ const accessState = useAccessState(action.access);
112
+ const isDisabled = action.disabled || accessState.disabled || accessState.readonly;
113
+
114
+ return (
115
+ <li role="none">
116
+ <button
117
+ type="button"
118
+ role="menuitem"
119
+ disabled={isDisabled}
120
+ onClick={() => {
121
+ onClose();
122
+ action.onClick(row);
123
+ }}
124
+ className={cn(
125
+ 'flex items-center gap-2 w-full text-left',
126
+ 'px-3 py-1.5 rounded-md text-sm',
127
+ // Neutral palette auto-inverts; danger/warning are color
128
+ // palettes (no auto-invert) so they keep their `dark:` shift
129
+ // intentionally — design choice, not a bug.
130
+ 'hover:bg-neutral-100',
131
+ 'focus:outline-none focus-visible:bg-neutral-100',
132
+ 'transition-colors motion-reduce:transition-none',
133
+ 'disabled:opacity-50 disabled:pointer-events-none',
134
+ action.color === 'danger' && 'text-danger-700 dark:text-danger-300',
135
+ action.color === 'warning' && 'text-warning-700 dark:text-warning-300',
136
+ )}
137
+ >
138
+ {action.icon && (
139
+ <span className="inline-flex shrink-0" aria-hidden="true">
140
+ {action.icon}
141
+ </span>
142
+ )}
143
+ <span>{action.label}</span>
144
+ </button>
145
+ </li>
146
+ );
147
+ }
@@ -0,0 +1,14 @@
1
+ export { RenderText } from './RenderText.js';
2
+ export type { RenderTextProps } from './RenderText.js';
3
+
4
+ export { RenderTwoLine } from './RenderTwoLine.js';
5
+ export type { RenderTwoLineProps } from './RenderTwoLine.js';
6
+
7
+ export { RenderChip } from './RenderChip.js';
8
+ export type { RenderChipProps } from './RenderChip.js';
9
+
10
+ export { RenderButton } from './RenderButton.js';
11
+ export type { RenderButtonProps } from './RenderButton.js';
12
+
13
+ export { RowActionsMenu } from './RowActionsMenu.js';
14
+ export type { RowActionsMenuProps, TableRowAction } from './RowActionsMenu.js';
@@ -0,0 +1,337 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AccessRequirement } from '@dashforge/rbac';
3
+ import type { TableVariants } from './table.variants.js';
4
+
5
+ // ───── Nested key util ─────
6
+
7
+ /**
8
+ * Type-level dotted path through an object's nested structure.
9
+ *
10
+ * `NestedKeyOf<{ user: { name: string } }>` → `'user' | 'user.name'`.
11
+ *
12
+ * Used as the `field` of a column to give autocomplete on nested
13
+ * paths. The runtime lookup happens via `getNestedValue` in
14
+ * `_internal/getNestedValue.ts` (split on `.` and walk).
15
+ */
16
+ export type NestedKeyOf<T extends object> = {
17
+ [K in keyof T & (string | number)]: T[K] extends object | undefined
18
+ ? T[K] extends ReadonlyArray<unknown>
19
+ ? `${K}`
20
+ : NonNullable<T[K]> extends object
21
+ ? `${K}` | `${K}.${NestedKeyOf<NonNullable<T[K]>>}`
22
+ : `${K}`
23
+ : `${K}`;
24
+ }[keyof T & (string | number)];
25
+
26
+ // ───── Column types ─────
27
+
28
+ /**
29
+ * Auto-detected primitive type for a column. Drives smart defaults:
30
+ * - `number` → align right + monospace (`tabular-nums`)
31
+ * - `date` → align left + monospace (consistent ISO width)
32
+ * - `boolean` → align center
33
+ * - `string` → align left (default)
34
+ * - `unknown` → fallback to left, no monospace
35
+ */
36
+ export type TableColumnInferredType =
37
+ | 'number'
38
+ | 'date'
39
+ | 'boolean'
40
+ | 'string'
41
+ | 'unknown';
42
+
43
+ /**
44
+ * Context passed to a custom `cellRenderer`. Mirrors what the consumer
45
+ * needs to render a cell: row, resolved value, position, selection
46
+ * state, expand state, and the resolved RBAC access state for THIS
47
+ * column.
48
+ */
49
+ export interface TableCellContext<T> {
50
+ row: T;
51
+ value: unknown;
52
+ rowIndex: number;
53
+ isSelected: boolean;
54
+ isExpanded: boolean;
55
+ /** Resolved RBAC state for this column. `null` if no `access` set. */
56
+ access: { hidden: boolean; disabled: boolean; readonly: boolean } | null;
57
+ }
58
+
59
+ /**
60
+ * One column definition for `<Table>`.
61
+ *
62
+ * **i18n**: pass `t('users.name')` directly to `header` — `header`
63
+ * accepts `string` for the common case. Use the function form
64
+ * `header: () => <span>…</span>` only when you need richer JSX.
65
+ *
66
+ * **Smart defaults**: when `align` / `monospace` are omitted, the
67
+ * Table auto-detects the column type from the first non-null value
68
+ * across the visible rows and applies `right + tabular-nums` for
69
+ * numbers, `center` for booleans, etc. Explicit props always win.
70
+ */
71
+ export interface TableColumn<T extends object> {
72
+ /**
73
+ * Dotted path to the value in each row. Supports nested keys
74
+ * (`'address.city.zipCode'`) thanks to `NestedKeyOf<T>` typing +
75
+ * runtime `getNestedValue()`.
76
+ */
77
+ field: NestedKeyOf<T> | (string & {});
78
+
79
+ /**
80
+ * Visible header. Pass a translation string directly:
81
+ * `header: t('users.name')`. Use the function form for richer
82
+ * JSX (e.g. header with an icon).
83
+ */
84
+ header: string | (() => ReactNode);
85
+
86
+ /** Pixel width (`px`). Mutually informative with `flex`. */
87
+ width?: number;
88
+ /** Flex grow factor (CSS `flex: <n> 1 0`). */
89
+ flex?: number;
90
+ /** Minimum pixel width (for resizable columns — deferred to v1-bis). */
91
+ minWidth?: number;
92
+
93
+ /**
94
+ * Horizontal alignment. Default is auto-detected from value type
95
+ * (number → right, boolean → center, otherwise left).
96
+ */
97
+ align?: 'left' | 'right' | 'center';
98
+
99
+ /**
100
+ * Opt-in to a monospace **font family** for this column. The
101
+ * library never auto-applies `font-mono` — it would override the
102
+ * consumer's theme `font-sans`. Set `true` explicitly only when
103
+ * the design calls for a typewriter font (e.g. raw hex IDs).
104
+ *
105
+ * The dashforgePreset does NOT own the `fontFamily` axis; the
106
+ * consumer configures the mono stack in their own
107
+ * `tailwind.config.ts` (`theme.extend.fontFamily.mono`).
108
+ */
109
+ monospace?: boolean;
110
+
111
+ /**
112
+ * Apply `font-variant-numeric: tabular-nums` so every digit
113
+ * occupies the same width — clean alignment for currency / metric
114
+ * columns. Does NOT change the font family. Auto-applied for
115
+ * `number` and `date` columns; pass `false` explicitly to opt out
116
+ * or `true` to force on a string column.
117
+ */
118
+ tabularNums?: boolean;
119
+
120
+ /**
121
+ * Make the column sortable. Pass `true` for default comparator
122
+ * (works for string/number/date/boolean) or a custom
123
+ * `(a, b) => number`.
124
+ */
125
+ sortable?: boolean | ((a: T, b: T) => number);
126
+
127
+ /**
128
+ * Include this column in the global search filter.
129
+ * @default false
130
+ */
131
+ searchable?: boolean;
132
+
133
+ /**
134
+ * Enable per-column quick filter (text-based in v1).
135
+ * @default false
136
+ */
137
+ filterable?: boolean;
138
+
139
+ /**
140
+ * Custom cell renderer. Receives the full row + the resolved
141
+ * (potentially nested) value, the row index, selection state,
142
+ * expansion state, and the RBAC access state.
143
+ */
144
+ cellRenderer?: (ctx: TableCellContext<T>) => ReactNode;
145
+
146
+ /**
147
+ * Per-column RBAC. `onUnauthorized: 'hide'` removes the column
148
+ * entirely from header AND every row cell (a11y-correct).
149
+ * `disable` / `readonly` are forwarded to `cellRenderer` via
150
+ * `ctx.access` — the renderer is expected to honor them.
151
+ */
152
+ access?: AccessRequirement;
153
+
154
+ /** Reserved for v1-bis (column-visibility dialog). */
155
+ hideable?: boolean;
156
+ defaultHidden?: boolean;
157
+ }
158
+
159
+ // ───── State models ─────
160
+
161
+ export type TableSortDirection = 'asc' | 'desc';
162
+
163
+ export interface TableSortItem {
164
+ field: string;
165
+ direction: TableSortDirection;
166
+ }
167
+
168
+ /**
169
+ * Sort model. Single-column sort = array of length 1. Multi-column
170
+ * sort = array of N (typically up to 3). The order matters: first
171
+ * item is the primary sort key, subsequent items break ties.
172
+ */
173
+ export type TableSortModel = TableSortItem[];
174
+
175
+ export interface TableFilterItem {
176
+ field: string;
177
+ /** Text substring match. Future ops: `equals`, `in`, `gte`, `lte`, `between`. */
178
+ op: 'contains';
179
+ value: string;
180
+ }
181
+
182
+ export type TableFilterModel = TableFilterItem[];
183
+
184
+ export type TableRowSelectionMode = 'none' | 'single' | 'multiple';
185
+
186
+ // ───── i18n labels ─────
187
+
188
+ /**
189
+ * Internal default strings used by the Table for accessibility
190
+ * announcements, density toggle, search placeholder, etc. Pass the
191
+ * subset you need to translate; the rest fall back to English.
192
+ *
193
+ * For column headers, empty state, bulk actions and row action
194
+ * labels — pass the translated value directly via the matching
195
+ * prop (`header`, `emptyState`, `bulkActions`, `rowActions`).
196
+ */
197
+ export interface TableLabels {
198
+ searchPlaceholder?: string;
199
+ noData?: string;
200
+ loading?: string;
201
+ ariaSortNone?: string;
202
+ ariaSortAscending?: string;
203
+ ariaSortDescending?: string;
204
+ ariaSelectRow?: string;
205
+ ariaSelectAllRows?: string;
206
+ ariaExpandRow?: string;
207
+ ariaCollapseRow?: string;
208
+ /** "X selected" — `{count}` placeholder gets replaced. */
209
+ selectedCount?: string;
210
+ clearSelection?: string;
211
+ /** Per-column filter Popover. */
212
+ filterColumn?: string;
213
+ filterApply?: string;
214
+ filterClear?: string;
215
+ /** Density toggle. */
216
+ density?: string;
217
+ densityCompact?: string;
218
+ densityComfortable?: string;
219
+ densitySpacious?: string;
220
+ }
221
+
222
+ // ───── Slot props ─────
223
+
224
+ export interface TableSlotProps {
225
+ root?: { className?: string };
226
+ toolbar?: { className?: string };
227
+ search?: { className?: string };
228
+ scroll?: { className?: string };
229
+ table?: { className?: string };
230
+ thead?: { className?: string };
231
+ tbody?: { className?: string };
232
+ headerRow?: { className?: string };
233
+ headerCell?: { className?: string };
234
+ row?: { className?: string };
235
+ cell?: { className?: string };
236
+ emptyState?: { className?: string };
237
+ loadingOverlay?: { className?: string };
238
+ selectionCell?: { className?: string };
239
+ expandToggleCell?: { className?: string };
240
+ expandedRow?: { className?: string };
241
+ rowActionsCell?: { className?: string };
242
+ bulkActionFooter?: { className?: string };
243
+ }
244
+
245
+ // ───── Expandable rows config ─────
246
+
247
+ export interface TableExpandableConfig<T> {
248
+ render: (row: T) => ReactNode;
249
+ expandedRowIds?: string[];
250
+ onExpandChange?: (ids: string[]) => void;
251
+ }
252
+
253
+ // ───── Main props ─────
254
+
255
+ /**
256
+ * Props for `<Table>`.
257
+ *
258
+ * Hybrid declarative-first architecture: by default every state piece
259
+ * (sort, filter, search, selection, expansion) is internal. Pass the
260
+ * matching controlled prop + change handler to lift state up.
261
+ *
262
+ * Smart defaults: column types are auto-detected on first render —
263
+ * number columns get right-align + tabular-nums, dates get monospace,
264
+ * booleans get center-align, etc. Override via explicit `align` /
265
+ * `monospace` per column.
266
+ *
267
+ * Visual style: Stripe-inspired clean lines by default
268
+ * (`variant="lines"`), row actions revealed on hover, sticky header,
269
+ * `comfortable` density. Switch via variant / density / size props.
270
+ *
271
+ * i18n: column `header` accepts plain strings (use `t('...')`
272
+ * directly). All internal label strings configurable via `labels`
273
+ * prop with English defaults.
274
+ */
275
+ export interface TableProps<T extends object>
276
+ extends Pick<TableVariants, 'variant' | 'size' | 'density'> {
277
+ // ───── Data ─────
278
+ rows: T[];
279
+ cols: TableColumn<T>[];
280
+ /** Stable row id resolver. Default: positional index. */
281
+ getRowId?: (row: T, index: number) => string;
282
+
283
+ // ───── Sort ─────
284
+ sortModel?: TableSortModel;
285
+ onSortChange?: (model: TableSortModel) => void;
286
+ /** Default sort applied when uncontrolled. */
287
+ defaultSortModel?: TableSortModel;
288
+
289
+ // ───── Search ─────
290
+ enableSearch?: boolean;
291
+ searchQuery?: string;
292
+ onSearchQueryChange?: (q: string) => void;
293
+ searchPlaceholder?: string;
294
+ searchDebounceMs?: number;
295
+
296
+ // ───── Per-column filter ─────
297
+ filterModel?: TableFilterModel;
298
+ onFilterChange?: (model: TableFilterModel) => void;
299
+
300
+ // ───── Selection ─────
301
+ rowSelection?: TableRowSelectionMode;
302
+ selectedRowIds?: string[];
303
+ onSelectionChange?: (ids: string[]) => void;
304
+ /** Sticky bulk action footer (visible only when selection > 0). */
305
+ bulkActions?: (selectedRows: T[]) => ReactNode;
306
+
307
+ // ───── Expandable rows ─────
308
+ expandable?: TableExpandableConfig<T>;
309
+
310
+ // ───── Row actions (revealed on hover) ─────
311
+ rowActions?: (row: T) => ReactNode;
312
+
313
+ // ───── States ─────
314
+ loading?: boolean;
315
+ /** Number of skeleton rows shown while `loading=true`. @default 5 */
316
+ loadingRowCount?: number;
317
+ emptyState?: ReactNode;
318
+
319
+ // ───── A11Y ─────
320
+ /** Sticky header. @default true */
321
+ stickyHeader?: boolean;
322
+ /** Optional caption for screen readers. */
323
+ caption?: ReactNode;
324
+ /** Renders the caption visually too (otherwise sr-only). @default false */
325
+ showCaption?: boolean;
326
+
327
+ // ───── RBAC ─────
328
+ /** Table-level RBAC. `hide` returns null; `disable` greys everything. */
329
+ access?: AccessRequirement;
330
+
331
+ // ───── i18n ─────
332
+ labels?: TableLabels;
333
+
334
+ // ───── Customization escape hatches ─────
335
+ sx?: string;
336
+ slotProps?: TableSlotProps;
337
+ }