@xsolla/xui-table 0.151.0-pr273.1778117489

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 ADDED
@@ -0,0 +1,493 @@
1
+ # @xsolla/xui-table
2
+
3
+ A composable, **strictly headless** data table that exposes only the
4
+ structural primitives needed to render a table.
5
+
6
+ The package ships **only the structural primitives**: container, caption,
7
+ header, body, footer, rows, and cells (with sort + reveal-on-hover support).
8
+ Everything else — title blocks, filter panels, item counters, bulk-selection
9
+ bars, pagination, sorting, filtering, selection, empty/loading/error
10
+ placeholders — is composed by the consumer. The toolkit doesn't care
11
+ whether you use plain `useState`, TanStack Table, React Aria, Redux, or
12
+ server components.
13
+
14
+ Designed against the [Xsolla UI Kit Table](https://www.figma.com/design/m0qqLlCvR29fvqj0EzlLAU/Xsolla-UI-kit?node-id=22969-49282).
15
+
16
+ ## Surface
17
+
18
+ ```tsx
19
+ <Table>
20
+ <Table.Caption /> {/* optional — <caption>-equivalent */}
21
+ <Table.Header> {/* <thead>-equivalent (sticky) */}
22
+ <Table.Row>
23
+ <Table.Head /> {/* <th>-equivalent (sortable) */}
24
+ </Table.Row>
25
+ </Table.Header>
26
+ <Table.Body> {/* <tbody>-equivalent */}
27
+ <Table.Row>
28
+ <Table.Cell />
29
+ </Table.Row>
30
+ </Table.Body>
31
+ <Table.Footer /> {/* slot for pagination etc. */}
32
+ </Table>
33
+ ```
34
+
35
+ | Sub-component | Role | Notes |
36
+ |---|---|---|
37
+ | `Table` | `role="table"` container | Padding, radius, gap, theme |
38
+ | `Table.Caption` | Caption / description | Goes anywhere inside `<Table>`; muted text by default |
39
+ | `Table.Header` | `role="rowgroup"` (sticky) | Wraps a single `<Table.Row>` containing `<Table.Head>` cells |
40
+ | `Table.Body` | `role="rowgroup"` | Wraps body `<Table.Row>` nodes; optional `minRows` prop reserves a stable height for paginated tables (see "Stable height across pages") |
41
+ | `Table.Footer` | Pagination slot | Layout primitive only — drop in `<Pagination>`, `<ProgressStep>`, or your own |
42
+ | `Table.Row` | `role="row"` | Used inside both Header and Body; auto-detects context |
43
+ | `Table.Head` | `role="columnheader"` with `aria-sort` | Sortable via `sort` + `onSortToggle` |
44
+ | `Table.Cell` | `role="cell"` | Optional `revealOnHover` for action cells |
45
+
46
+ Density is single-source from Figma — there's no `size` prop. All
47
+ spacing, row heights, and font sizes come from the `theme.sizing.table`
48
+ token. Figma node `22969:49282` only ships one canonical density (`56 px`
49
+ rows, `24 px` horizontal padding, `14 px` body font); if denser variants
50
+ are added later, this will become a `size` prop.
51
+
52
+ ## What's NOT in the toolkit (and why)
53
+
54
+ | Pattern | Where it lives | Why |
55
+ |---|---|---|
56
+ | Title block (`<h1>` + description + actions) | Consumer | Every team's design system has its own typography scale and action layout |
57
+ | Filter panel (search + selects + view toggle) | Consumer | Filters change per page; coupling them to `<Table>` adds a config surface for no benefit |
58
+ | Item counter (`"N items"`) | Consumer | One `<Text aria-live="polite">` line — no need for a sub-component |
59
+ | Bulk-selection bar (`"N selected"` + actions) | Consumer | Mounted above `<Table>` when `selected.size > 0`; see the `Selectable` story |
60
+ | Pagination | `@xsolla/xui-pagination` (numeric) or `@xsolla/xui-stepper` (`<ProgressStep>` for dots) — drop into `<Table.Footer>` | Already separate components |
61
+ | Sorting / filtering / paging logic | Consumer (plain `useState`, TanStack Table, …) | Headless contract |
62
+ | Empty / loading / error placeholders | Consumer renders inside `<Table.Body>` | Every team wants different illustrations / copy / CTAs |
63
+
64
+ The philosophy is a tiny primitive surface and infinite composition on
65
+ the consumer side.
66
+
67
+ ## Headless contract
68
+
69
+ The component is fully **controlled**. None of the following is stored
70
+ internally — the consumer passes it every render:
71
+
72
+ | Prop | On | What you control |
73
+ |---|---|---|
74
+ | `sort="ascending" \| "descending" \| "none"` | `Table.Head` | Active sort direction for this column. Renders the symmetric dual-chevron `Sort` icon — full opacity when sorted (`"ascending"` / `"descending"`), 40% when sortable but inactive (`"none"`). The icon itself doesn't visually distinguish ascending from descending (Figma's spec); direction is announced to assistive tech via `aria-sort` (set automatically). |
75
+ | `onSortToggle` | `Table.Head` | Click handler for the sort indicator |
76
+ | `selected` | `Table.Row` | Whether the row is in the selected state |
77
+ | `onPress` | `Table.Row` | Row click handler |
78
+ | `hideDivider` | `Table.Row` | Suppress the bottom divider (typically on the last row) |
79
+ | `revealOnHover` | `Table.Cell` | Hide the cell until the parent row is hovered/focused |
80
+
81
+ ## Quick start
82
+
83
+ ```tsx
84
+ import { useState, useMemo } from "react";
85
+ import { Table } from "@xsolla/xui-table";
86
+ import { Pagination } from "@xsolla/xui-pagination";
87
+ import { Tag } from "@xsolla/xui-tag";
88
+
89
+ function PromotionsTable({ rows }) {
90
+ const [sortKey, setSortKey] = useState("title");
91
+ const [sortDir, setSortDir] = useState("ascending");
92
+ const [page, setPage] = useState(1);
93
+
94
+ const sorted = useMemo(() => {
95
+ if (sortDir === "none") return rows;
96
+ return [...rows].sort((a, b) =>
97
+ sortDir === "ascending"
98
+ ? a[sortKey].localeCompare(b[sortKey])
99
+ : b[sortKey].localeCompare(a[sortKey])
100
+ );
101
+ }, [rows, sortKey, sortDir]);
102
+
103
+ const cycleSort = (key) => {
104
+ if (sortKey !== key) {
105
+ setSortKey(key);
106
+ setSortDir("ascending");
107
+ } else if (sortDir === "ascending") setSortDir("descending");
108
+ else if (sortDir === "descending") setSortDir("none");
109
+ else setSortDir("ascending");
110
+ };
111
+
112
+ return (
113
+ <Table>
114
+ <Table.Header>
115
+ <Table.Row>
116
+ <Table.Head
117
+ sort={sortKey === "title" ? sortDir : "none"}
118
+ onSortToggle={() => cycleSort("title")}
119
+ >
120
+ Title
121
+ </Table.Head>
122
+ <Table.Head width={140}>Status</Table.Head>
123
+ <Table.Head width={140}>Created</Table.Head>
124
+ </Table.Row>
125
+ </Table.Header>
126
+ <Table.Body>
127
+ {sorted.map((row, i) => (
128
+ <Table.Row key={row.id} hideDivider={i === sorted.length - 1}>
129
+ <Table.Cell>{row.title}</Table.Cell>
130
+ <Table.Cell width={140}>
131
+ <Tag tone={row.tone} size="sm">
132
+ {row.status}
133
+ </Tag>
134
+ </Table.Cell>
135
+ <Table.Cell width={140}>{row.created}</Table.Cell>
136
+ </Table.Row>
137
+ ))}
138
+ </Table.Body>
139
+ <Table.Footer>
140
+ <Pagination
141
+ currentPage={page}
142
+ totalPages={5}
143
+ onPageChange={setPage}
144
+ />
145
+ </Table.Footer>
146
+ </Table>
147
+ );
148
+ }
149
+ ```
150
+
151
+ ## Plugging in TanStack Table (or any other engine)
152
+
153
+ `@xsolla/xui-table` does **not** depend on TanStack — it's a consumer
154
+ choice. The toolkit renders rows; TanStack tells you which rows to render.
155
+
156
+ ```tsx
157
+ import {
158
+ useReactTable,
159
+ getCoreRowModel,
160
+ getSortedRowModel,
161
+ flexRender,
162
+ } from "@tanstack/react-table";
163
+
164
+ function TanStackPromotionsTable({ data, columns }) {
165
+ const table = useReactTable({
166
+ data,
167
+ columns,
168
+ getCoreRowModel: getCoreRowModel(),
169
+ getSortedRowModel: getSortedRowModel(),
170
+ });
171
+
172
+ return (
173
+ <Table>
174
+ <Table.Header>
175
+ {table.getHeaderGroups().map((hg) => (
176
+ <Table.Row key={hg.id}>
177
+ {hg.headers.map((h) => (
178
+ <Table.Head
179
+ key={h.id}
180
+ sort={h.column.getIsSorted() || "none"}
181
+ onSortToggle={h.column.getToggleSortingHandler()}
182
+ >
183
+ {flexRender(h.column.columnDef.header, h.getContext())}
184
+ </Table.Head>
185
+ ))}
186
+ </Table.Row>
187
+ ))}
188
+ </Table.Header>
189
+ <Table.Body>
190
+ {table.getRowModel().rows.map((row, i, all) => (
191
+ <Table.Row key={row.id} hideDivider={i === all.length - 1}>
192
+ {row.getVisibleCells().map((cell) => (
193
+ <Table.Cell key={cell.id}>
194
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
195
+ </Table.Cell>
196
+ ))}
197
+ </Table.Row>
198
+ ))}
199
+ </Table.Body>
200
+ </Table>
201
+ );
202
+ }
203
+ ```
204
+
205
+ ## Composing surrounding UI on the consumer side
206
+
207
+ Most production tables wrap extra UI around the data: a title block, a
208
+ filter panel, an item counter, a bulk-selection bar, and pagination. The
209
+ toolkit ships none of those — they're plain JSX. There are **two valid
210
+ positions** for this surrounding UI:
211
+
212
+ ### A. Inside `<Table>`, above `<Table.Header>` (matches Figma)
213
+
214
+ The Figma "Filter panel" group sits inside the same rounded card as the
215
+ header and rows. Achieve this by dropping the surrounding UI as direct
216
+ children of `<Table>`.
217
+
218
+ `<Table>` ships **flush-stack** defaults (`containerPaddingVertical: 0`,
219
+ `containerGap: 0`) — its rowgroups (Header / Body / Footer) sit
220
+ edge-to-edge inside the card and the dividers do the visual separation.
221
+ Figma's master shows 24 px top/bottom padding and a 16 px gap between
222
+ groups, but that spacing only exists to host the filter-panel / counter
223
+ blocks; reusing it for rowgroups produces visible gaps between Header
224
+ and Body that look broken.
225
+
226
+ So each surrounding block composes its own spacing: wrap them in a
227
+ column-flex `<Box>` with `gap={16}`, give the wrapper `paddingTop={24}`
228
+ so it doesn't sit flush against the rounded card top edge, and have
229
+ each block use `paddingHorizontal={24}` to align with header / cell
230
+ padding. The Footer adds `paddingTop={16}` + `paddingBottom={24}` for
231
+ the same reason at the bottom.
232
+
233
+ ```tsx
234
+ <Table>
235
+ {/* Wrapper owns gap + top padding */}
236
+ <Box flexDirection="column" gap={16} paddingTop={24}>
237
+ {/* Title block */}
238
+ <Box flexDirection="row" justifyContent="space-between" paddingHorizontal={24}>
239
+ <Box flexDirection="column" gap={4}>
240
+ <Text fontSize={24} fontWeight="600">Promotions</Text>
241
+ <Text fontSize={14} style={{ opacity: 0.6 }}>
242
+ Manage active and draft promotions
243
+ </Text>
244
+ </Box>
245
+ <Box flexDirection="row" gap={8}>
246
+ <Button variant="secondary">Export</Button>
247
+ <Button>New promotion</Button>
248
+ </Box>
249
+ </Box>
250
+
251
+ {/* Filter panel */}
252
+ <Box flexDirection="row" paddingHorizontal={24}>
253
+ <Input placeholder="Search" iconLeft={<Search size={18} />} />
254
+ </Box>
255
+
256
+ {/* Item counter */}
257
+ <Box flexDirection="row" justifyContent="space-between" paddingHorizontal={24}>
258
+ <Text aria-live="polite" style={{ opacity: 0.6 }}>{rows.length} items</Text>
259
+ <FlexButton variant="tertiary" size="sm" iconRight={<Reset size={14} />}>
260
+ Reset filters
261
+ </FlexButton>
262
+ </Box>
263
+ </Box>
264
+
265
+ <Table.Header>
266
+ <Table.Row>
267
+ <Table.Head>Title</Table.Head>
268
+ ...
269
+ </Table.Row>
270
+ </Table.Header>
271
+ <Table.Body>
272
+ {rows.map((row) => (
273
+ // No `hideDivider` on the last row — the divider visually
274
+ // separates the body from the footer below.
275
+ <Table.Row key={row.id}>...</Table.Row>
276
+ ))}
277
+ </Table.Body>
278
+ <Table.Footer paddingTop={16} paddingBottom={24}>
279
+ <Pagination ... />
280
+ </Table.Footer>
281
+ </Table>
282
+ ```
283
+
284
+ `<Table.Header>` paints a horizontal divider _below_ itself, so the
285
+ filter row above is visually separated from the column-header row
286
+ without the consumer needing to add their own divider.
287
+
288
+ ### B. Outside `<Table>`, above the card
289
+
290
+ Useful when the bulk-selection bar should appear *above* the rounded card
291
+ (common for "X selected" toolbars that visually replace a page-level
292
+ header):
293
+
294
+ ```tsx
295
+ <Box flexDirection="column" gap={12}>
296
+ {selected.size > 0 && (
297
+ <Box role="region" aria-live="polite" /* …action layout… */>
298
+ <Text>{selected.size} selected</Text>
299
+ <Button>Delete</Button>
300
+ </Box>
301
+ )}
302
+ <Table>
303
+ <Table.Header>...</Table.Header>
304
+ <Table.Body>...</Table.Body>
305
+ </Table>
306
+ </Box>
307
+ ```
308
+
309
+ See the `Selectable` story.
310
+
311
+ ## Pagination — pick the right component
312
+
313
+ Drop either of these into `<Table.Footer>`:
314
+
315
+ | Component | Use when |
316
+ |---|---|
317
+ | `<Pagination>` | Numeric pages — `Prev` / `1 2 3 …` / `Next` |
318
+ | `<ProgressStep>` (from `@xsolla/xui-stepper`) | Dot-style "step N of M" pagination |
319
+
320
+ `<Pagination>` is 1-indexed (`currentPage`, `totalPages`); `<ProgressStep>`
321
+ is 0-indexed (`activeStep`, `count`). When swapping between them, adjust
322
+ by `±1`.
323
+
324
+ ## Stable height across pages (`Table.Body minRows`)
325
+
326
+ Paginated tables have three states that naturally render at different
327
+ heights and cause the card to jump:
328
+
329
+ 1. **Full pages** — `pageSize` rows, divider stack between them.
330
+ 2. **Partial last page** — fewer than `pageSize` rows.
331
+ 3. **No-results state** — a single empty-state block.
332
+
333
+ Pass `minRows={pageSize}` to `<Table.Body>` and the body locks itself to
334
+ the full page height for all three:
335
+
336
+ ```tsx
337
+ <Table>
338
+ <Table.Header>{/* … */}</Table.Header>
339
+ <Table.Body minRows={pageSize}>
340
+ {rows.length === 0 ? (
341
+ <Box style={{ flex: 1, /* …centered no-results message… */ }}>
342
+ No results
343
+ </Box>
344
+ ) : (
345
+ rows.map((row, i) => (
346
+ <Table.Row key={row.id} hideDivider={i === rows.length - 1}>
347
+ {/* …cells… */}
348
+ </Table.Row>
349
+ ))
350
+ )}
351
+ </Table.Body>
352
+ <Table.Footer>{/* …pagination… */}</Table.Footer>
353
+ </Table>
354
+ ```
355
+
356
+ Behavior:
357
+
358
+ - The body reserves a `min-height` of
359
+ `minRows × rowHeight + (minRows - 1) × dividerHeight` from
360
+ `theme.sizing.table`.
361
+ - When all children are `<Table.Row>` and there are fewer of them than
362
+ `minRows`, invisible placeholder slots are appended to fill the
363
+ difference. The last real row keeps its divider visible (mirroring how
364
+ a fully filled page looks); the last placeholder slot drops the
365
+ `+1px` divider so the body bottom matches a full page exactly.
366
+ - When the body holds a non-row child (e.g. your custom no-results
367
+ panel), no placeholders are appended — only the `min-height` is
368
+ enforced, so you can stretch the panel via `flex: 1` or `height:
369
+ 100%`.
370
+
371
+ Always render `<Table.Footer>` in the same conditional state, otherwise
372
+ the footer disappearing on the no-results state will reintroduce a
373
+ height jump.
374
+
375
+ ## Hide row actions until hover (`revealOnHover`)
376
+
377
+ ```tsx
378
+ <Table.Row>
379
+ <Table.Cell>{row.title}</Table.Cell>
380
+ {/* …other cells… */}
381
+ <Table.Cell width={48} position="right" revealOnHover>
382
+ <IconButton icon={<MoreVr />} aria-label={`Actions for ${row.title}`} />
383
+ </Table.Cell>
384
+ </Table.Row>
385
+ ```
386
+
387
+ The cell is hidden via `opacity: 0` + `pointer-events: none` (not
388
+ `visibility: hidden`), so keyboard Tab still focuses descendants. Focusing
389
+ a descendant flips the row's `focus-within` state and reveals the cell.
390
+ On touch-only devices the cell is always visible — matches Figma's
391
+ "Right cell actions are always visible on touch" spec.
392
+
393
+ ## Truncation + tooltip on long values
394
+
395
+ `<Table.Cell>` and `<Table.Head>` wrap string children in a single-line
396
+ `<Text>` with `text-overflow: ellipsis`. For long values, wrap them in a
397
+ `<Tooltip>` so the full text is reachable:
398
+
399
+ ```tsx
400
+ <Table.Cell>
401
+ <Tooltip content={row.title}>
402
+ <span>{row.title}</span>
403
+ </Tooltip>
404
+ </Table.Cell>
405
+ ```
406
+
407
+ ## Loading: skeleton rows, not spinners
408
+
409
+ Render the same number of rows you expect to receive, with skeleton blocks
410
+ inside each `<Table.Cell>`. Column widths stay fixed → no layout shift
411
+ when real data arrives. See the `LoadingState` story.
412
+
413
+ ## Empty cells: `aria-label` for `—`
414
+
415
+ ```tsx
416
+ <Table.Cell aria-label="No value">—</Table.Cell>
417
+ ```
418
+
419
+ Keeps screen reader output meaningful.
420
+
421
+ ## Scrolling
422
+
423
+ The table is unopinionated about scroll. Wrap it (or the part you want to
424
+ scroll) in a sized container.
425
+
426
+ ### Vertical scroll with sticky header
427
+
428
+ `<Table.Header>` already uses `position: sticky; top: 0`. Wrap
429
+ `<Table.Body>` in a fixed-height `overflow-y: auto` container and the
430
+ header stays pinned while rows scroll. See the `Scrollable` story.
431
+
432
+ ### Horizontal scroll
433
+
434
+ `<Table>` is intentionally opaque — it doesn't accept `style` or
435
+ Box-level layout props, so the scroll viewport lives **outside**.
436
+ Two-Box wrapper: the outer Box scrolls horizontally and the inner uses
437
+ `min-width: max-content` so the table card grows to the natural width of
438
+ its widest row.
439
+
440
+ ```tsx
441
+ <Box style={{ width: "100%", overflowX: "auto" }}>
442
+ <Box style={{ minWidth: "max-content" }}>
443
+ <Table>...</Table>
444
+ </Box>
445
+ </Box>
446
+ ```
447
+
448
+ Don't hard-code a pixel `min-width`. The actual row width is
449
+ `sum(cell widths) + (n - 1) × cellGap + 2 × rowPaddingHorizontal`, which
450
+ almost always undershoots if you eyeball it — and any miss leaves the
451
+ cells spilling outside the card's white background when you scroll.
452
+ `max-content` lets the browser compute the right value so the card and
453
+ the cells stay the same width. See the `HorizontalScroll` story.
454
+
455
+ ### Frozen first column (optional)
456
+
457
+ Wrap the first cell in `position: sticky; left: 0` with a matching
458
+ background color and z-index. The toolkit doesn't ship a `freezeFirst`
459
+ prop because the visual treatment (border, shadow, background) varies per
460
+ team.
461
+
462
+ ## Virtualization
463
+
464
+ Not built in. Either:
465
+
466
+ - Drop a virtualization wrapper around `<Table.Body>`'s children
467
+ (`@tanstack/react-virtual`, `react-window`), or
468
+ - Rely on server-side pagination via `<Table.Footer>` + `<Pagination>`.
469
+
470
+ Row height is fixed (`theme.sizing.table.rowHeight`, 56 px), so
471
+ fixed-height virtualizers work out of the box.
472
+
473
+ ## Platform support
474
+
475
+ **Web only.** The package builds a `dist/native/` bundle to match the
476
+ workspace convention, but `Table` relies on behavior that doesn't exist
477
+ on React Native (`position: sticky`, DOM mouse and focus events, CSS
478
+ transitions). React Native is not supported.
479
+
480
+ For native screens, pair `FlatList` with your own row + cell components.
481
+
482
+ ## At a glance
483
+
484
+ | | `@xsolla/xui-table` |
485
+ |---|---|
486
+ | Surface | `Table`, `Table.Caption`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell` |
487
+ | State | None — bring your own (plain `useState`, TanStack Table, …) |
488
+ | Sort indicator | Built into `Table.Head` (symmetric dual-chevron + `aria-sort` for direction) |
489
+ | Hover-reveal cells | `revealOnHover` prop on `Table.Cell` |
490
+ | Sticky header | Built into `Table.Header` |
491
+ | Theming | Toolkit theme tokens (`theme.sizing.table`) |
492
+ | Cross-platform | Web only |
493
+ | Bundles a row data engine | No |
@@ -0,0 +1,122 @@
1
+ import * as React from 'react';
2
+ import React__default, { ReactNode } from 'react';
3
+ import { BoxProps } from '@xsolla/xui-primitives-core';
4
+ import { ThemeOverrideProps } from '@xsolla/xui-core';
5
+
6
+ type TableCellAlign = "left" | "right" | "center";
7
+ type TableCellPosition = "default" | "left" | "right";
8
+ /**
9
+ * Structural primitives only:
10
+ *
11
+ * <Table>
12
+ * <Table.Caption /> ← optional <caption>-equivalent for a11y
13
+ * <Table.Header> ← <thead>-equivalent (sticky)
14
+ * <Table.Row>
15
+ * <Table.Head /> ← <th>-equivalent (sortable)
16
+ * </Table.Row>
17
+ * </Table.Header>
18
+ * <Table.Body> ← <tbody>-equivalent
19
+ * <Table.Row>
20
+ * <Table.Cell />
21
+ * </Table.Row>
22
+ * </Table.Body>
23
+ * <Table.Footer /> ← slot for pagination etc.
24
+ * </Table>
25
+ *
26
+ * Filter panels, item counters, title blocks, bulk-selection bars, and
27
+ * pagination components are NOT part of the surface — compose them on the
28
+ * consumer side around <Table>.
29
+ *
30
+ * Density is single-source from Figma (`theme.sizing.table`); no `size`
31
+ * prop. If Figma ever ships md/sm variants, this will become a function
32
+ * with a size argument again.
33
+ */
34
+ /**
35
+ * `<Table>` is intentionally opaque: no `style`, no Box-level props. The
36
+ * surface is `children` only (plus theme overrides). To wrap the table in
37
+ * a horizontal-scroll viewport, compose two outer Boxes around it — see
38
+ * the `HorizontalScroll` story or the README "Horizontal scroll" section.
39
+ */
40
+ interface TableProps extends ThemeOverrideProps {
41
+ children?: ReactNode;
42
+ }
43
+ interface TableCaptionProps extends BoxProps, ThemeOverrideProps {
44
+ children?: ReactNode;
45
+ }
46
+ interface TableHeaderProps extends BoxProps, ThemeOverrideProps {
47
+ children?: ReactNode;
48
+ }
49
+ interface TableBodyProps extends BoxProps, ThemeOverrideProps {
50
+ children?: ReactNode;
51
+ /**
52
+ * Reserve space for at least this many row slots. Useful for paginated
53
+ * tables where partial last pages or empty filter results would
54
+ * otherwise make the card height jump between states.
55
+ *
56
+ * When the body contains only `<Table.Row>` children and there are
57
+ * fewer of them than `minRows`, invisible placeholder slots are
58
+ * appended to fill the difference, and the last real row keeps its
59
+ * divider visible (mirroring a fully filled page).
60
+ *
61
+ * If the body contains non-row children (e.g. a custom empty-state
62
+ * block), no placeholders are appended — the body just reserves the
63
+ * full height via `min-height` so the consumer can stretch their
64
+ * empty-state to fill it.
65
+ */
66
+ minRows?: number;
67
+ }
68
+ interface TableFooterProps extends BoxProps {
69
+ children?: ReactNode;
70
+ }
71
+ interface TableRowProps extends BoxProps, ThemeOverrideProps {
72
+ children?: ReactNode;
73
+ /** Highlight the row on hover. Defaults to true (body rows only). */
74
+ hoverable?: boolean;
75
+ /** Marks the row as selected. */
76
+ selected?: boolean;
77
+ /** Hide the bottom divider for this row. */
78
+ hideDivider?: boolean;
79
+ /** Click handler. When provided, the row becomes interactive. */
80
+ onPress?: () => void;
81
+ /**
82
+ * Optional focus events. The Row uses these internally to track
83
+ * focus-within so that descendant `revealOnHover` cells can show
84
+ * themselves when keyboard-focused; consumer handlers (if provided) are
85
+ * still invoked.
86
+ */
87
+ onFocus?: (e: React.FocusEvent<HTMLElement>) => void;
88
+ onBlur?: (e: React.FocusEvent<HTMLElement>) => void;
89
+ }
90
+ interface TableCellProps extends Omit<BoxProps, "position">, ThemeOverrideProps {
91
+ children?: ReactNode;
92
+ /** Cell text alignment. Defaults to "left". */
93
+ align?: TableCellAlign;
94
+ /** Position within the row — controls padding (left = leading, right = trailing). */
95
+ position?: TableCellPosition;
96
+ /** Allow the cell to flex to fill space. */
97
+ grow?: number;
98
+ /**
99
+ * Hide the cell until the parent row is hovered or focused (via `:focus-within`).
100
+ * Typically used for `position="right"` action cells per Figma spec.
101
+ * Falls back to always-visible on touch devices.
102
+ */
103
+ revealOnHover?: boolean;
104
+ }
105
+ interface TableHeadProps extends TableCellProps {
106
+ /** Sort direction. */
107
+ sort?: "ascending" | "descending" | "none";
108
+ /** Click to toggle sort. */
109
+ onSortToggle?: () => void;
110
+ }
111
+
112
+ declare const Table: React__default.ForwardRefExoticComponent<TableProps & React__default.RefAttributes<any>> & {
113
+ Caption: React__default.ForwardRefExoticComponent<TableCaptionProps & React__default.RefAttributes<any>>;
114
+ Header: React__default.ForwardRefExoticComponent<TableHeaderProps & React__default.RefAttributes<any>>;
115
+ Body: React__default.ForwardRefExoticComponent<TableBodyProps & React__default.RefAttributes<any>>;
116
+ Footer: React__default.ForwardRefExoticComponent<TableFooterProps & React__default.RefAttributes<any>>;
117
+ Row: React__default.ForwardRefExoticComponent<TableRowProps & React__default.RefAttributes<any>>;
118
+ Head: React__default.ForwardRefExoticComponent<TableHeadProps & React__default.RefAttributes<any>>;
119
+ Cell: React__default.ForwardRefExoticComponent<TableCellProps & React__default.RefAttributes<any>>;
120
+ };
121
+
122
+ export { Table, type TableBodyProps, type TableCaptionProps, type TableCellAlign, type TableCellPosition, type TableCellProps, type TableFooterProps, type TableHeadProps, type TableHeaderProps, type TableProps, type TableRowProps };