@spark-web/data-table 5.2.0 → 5.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # @spark-web/data-table
2
2
 
3
+ ## 5.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#782](https://github.com/brighte-labs/spark-web/pull/782)
8
+ [`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)
9
+ Thanks [@jacobporci-brighte](https://github.com/jacobporci-brighte)! -
10
+ **docs:** add CLAUDE.md AI context files to foundation and form packages;
11
+ patch data-table with manual sort and spinner overlay patterns
12
+
13
+ - [#782](https://github.com/brighte-labs/spark-web/pull/782)
14
+ [`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)
15
+ Thanks [@jacobporci-brighte](https://github.com/jacobporci-brighte)! -
16
+ **data-table:** add `isLoading` and `emptyState` props — renders a spinner +
17
+ "Loading" text or a custom empty node below the table; table now fills its
18
+ container width by default (`width: 100%`)
19
+
20
+ - [#782](https://github.com/brighte-labs/spark-web/pull/782)
21
+ [`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)
22
+ Thanks [@jacobporci-brighte](https://github.com/jacobporci-brighte)! -
23
+ **data-table:** Fix token gaps in CLAUDE.md (header cell padding, font-family,
24
+ loading/empty state containers, sort icon margin); add Composition section;
25
+ clarify row hover only applies to clickable rows and must be suppressed when
26
+ hovering a button inside the row
27
+
28
+ **design-system:** Update internal-admin surface rules with MeatballMenu hover
29
+ suppression rule; fix list-page.md MultiSelect label pattern (gap and text
30
+ size were contradicting the rules text); update pattern file code examples to
31
+ match
32
+
33
+ **meatball-menu:** Rewrite CLAUDE.md with full structure — What this is NOT,
34
+ complete token usage table, Composition section, and Do NOTs
35
+
36
+ **status-badge:** Rewrite CLAUDE.md with full structure — What this is NOT,
37
+ complete token usage table with per-tone background mapping, Composition
38
+ section, and Do NOTs
39
+
40
+ - [#782](https://github.com/brighte-labs/spark-web/pull/782)
41
+ [`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)
42
+ Thanks [@jacobporci-brighte](https://github.com/jacobporci-brighte)! -
43
+ **data-table:** align color tokens with @spark-web/table conventions — use
44
+ `primarySoft` for selected rows, `inputPressed` for hover, `primaryActive` for
45
+ header text/border; switch sort icons to `SortAscending/DescendingIcon`; add
46
+ `aria-sort` accessibility attribute; add CLAUDE.md AI context
47
+ - Updated dependencies
48
+ [[`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)]:
49
+ - @spark-web/box@6.0.1
50
+ - @spark-web/spinner@5.1.1
51
+ - @spark-web/text@5.3.1
52
+
3
53
  ## 5.2.0
4
54
 
5
55
  ### Minor Changes
package/CLAUDE.md ADDED
@@ -0,0 +1,217 @@
1
+ # @spark-web/data-table — AI Context
2
+
3
+ ## What this package is
4
+
5
+ A headless data table built on TanStack React Table v8. One component
6
+ (`DataTable`) accepts column definitions and data, and renders a styled table
7
+ with support for row selection, row expansion, column visibility, sorting, and
8
+ infinite scroll.
9
+
10
+ ## What this package is NOT
11
+
12
+ - Not responsible for data fetching, sorting logic, filtering, or pagination.
13
+ The parent owns those — `DataTable` surfaces sort state changes via
14
+ `onSortingChange`.
15
+ - Not composable. `DataTable` is a single monolithic component — there are no
16
+ composable sub-component alternatives in this design system.
17
+
18
+ ## Component API
19
+
20
+ `DataTable<T>` accepts:
21
+
22
+ - `items: Array<T>` **(required)** — data rows
23
+ - `columns: ColumnDef<T>[]` **(required)** — TanStack column definitions
24
+ - `isLoading?: boolean` — controls the loading state row below the table body
25
+ - `emptyState?: ReactNode` — rendered when `isLoading` is false and `items` is
26
+ empty
27
+ - `state` — optional controlled state: `rowSelection`, `expanded`,
28
+ `columnVisibility`, `sorting`
29
+ - `onRowSelectionChange` — enables row selection
30
+ - `onRowClick` / `enableClickableRow` — row click behaviour
31
+ - `onSortingChange` + `enableSorting` + `manualSorting` — sorting
32
+ - `onExpandedChange` + `enableExpanding` + `expandedRowComponent` — expansion
33
+ - `showBottomSpinner` + `onBottomSpinnerShown` — infinite scroll trigge
34
+ - `renderRow` — full custom tbody replacement
35
+ - `className` / `headerClassName` / `footerClassName` / `rowClassName` — style
36
+ overrides
37
+
38
+ Always provide `isLoading` and `emptyState` in production usage — omitting them
39
+ leaves the table with no loading indicator or empty state feedback.
40
+
41
+ ## Token usage
42
+
43
+ All values from `useTheme()` from `@spark-web/theme`. Never use raw hex, px, or
44
+ Tailwind.
45
+
46
+ ### Row states
47
+
48
+ - Default: no background override (inherits surface)
49
+ - Selected/expanded: `theme.color.background.primarySoft`
50
+ - Hover: `theme.color.background.inputPressed` — applied only when
51
+ `enableClickableRow` is `true`. NEVER apply row hover to a non-clickable row.
52
+ - Hover suppression: when a row contains a `MeatballMenu`, hovering over the
53
+ meatball button must NOT trigger the row hover. Implement using CSS `:has()`:
54
+ ```css
55
+ ':hover:not(:has(button:hover))': {
56
+ backgroundcolor: theme.color.background.inputPressed;
57
+ }
58
+ ```
59
+ This ensures the row highlight disappears when the cursor moves over any
60
+ button within the row, including the meatball trigger.
61
+
62
+ ### Header
63
+
64
+ - Background: `theme.color.background.surface`
65
+ - Border bottom: `theme.color.background.primaryDark` (via inset box-shadow:
66
+ `inset 0 -2px 0 0 ${theme.color.background.primaryDark}`)
67
+ - Cell padding: `theme.spacing.medium` (via Box `padding="medium"` on `<th>`)
68
+ - Text color: `theme.color.background.primaryDark`
69
+ - Text transform: `capitalize` (inline CSS on `th`)
70
+ - Sort icon stroke: `theme.color.background.primaryDark` (inline CSS on `svg`)
71
+ - Font family: `theme.typography.fontFamily.sans.name`
72
+ - Font weight: `theme.typography.fontWeight.semibold`
73
+ - Sortable header hover: `theme.backgroundInteractions.primaryLowHover`
74
+
75
+ ### Cell
76
+
77
+ - Padding: `theme.spacing.large` (all sides, via Box `padding="large"` on
78
+ `<td>`)
79
+ - Border bottom: `theme.border.width.standard` solid
80
+ `theme.border.color.standard`
81
+ - Vertical align: `middle` (inline CSS)
82
+ - Text align: `left` (inline CSS)
83
+
84
+ ### Sort icons
85
+
86
+ Use `SortAscendingIcon` / `SortDescendingIcon` from `@spark-web/icon` at
87
+ `size="xxsmall"` `tone="primaryActive"`. Never `ChevronUpIcon` /
88
+ `ChevronDownIcon` for sort state.
89
+
90
+ - Icon left margin: `theme.spacing.xsmall` (inline CSS on the `svg` selector)
91
+
92
+ ### Loading state (`isLoading`)
93
+
94
+ Renders below the table body when `isLoading` is `true`. Does not overlay the
95
+ table — replaces visible data feedback.
96
+
97
+ - Container padding: `padding="xlarge"` (Box prop)
98
+ - Container gap: `gap="small"` (Box prop)
99
+ - Container alignment: `alignItems="center"`, `justifyContent="center"` (Box
100
+ props)
101
+ - Text: `<Text tone="muted">Loading</Text>`
102
+ - Spinner: `<Spinner tone="primary" />` — no explicit size; uses Spinner default
103
+
104
+ ### Empty state (`emptyState`)
105
+
106
+ Renders below the table body when `isLoading` is `false` and `items` is empty.
107
+
108
+ - Container padding: `padding="xlarge"` (Box prop)
109
+ - Container alignment: `justifyContent="center"` (Box prop)
110
+ - Content: caller-provided `ReactNode` — always pass
111
+ `<Text tone="muted" size="small">…</Text>`
112
+
113
+ ## Composition
114
+
115
+ - `Box` from `@spark-web/box` — outer wrapper, `<table>`, `<thead>`, `<tbody>`,
116
+ `<tfoot>`, `<tr>`, `<th>`, `<td>` elements
117
+ - `SortAscendingIcon` / `SortDescendingIcon` from `@spark-web/icon` — sort state
118
+ icons in sortable header cells
119
+ - `Spinner` from `@spark-web/spinner` — loading overlay (via `isLoading`) and
120
+ infinite-scroll bottom spinner (via `showBottomSpinner`)
121
+ - `Text` from `@spark-web/text` — "Loading" label in `isLoading` state
122
+ - `InView` from `react-intersection-observer` — triggers `onBottomSpinnerShown`
123
+ when the bottom spinner enters the viewport
124
+
125
+ ## Column definitions
126
+
127
+ Use `createColumnHelper<T>()` from this package for type-safe column defs:
128
+
129
+ ```tsx
130
+ import { createColumnHelper } from '@spark-web/data-table';
131
+ const col = createColumnHelper<MyType>();
132
+ ```
133
+
134
+ Cell renderers receive `info` (`CellContext`) — use `info.getValue()` or
135
+ `info.renderValue()`. Wrap cell content in `<Text size="small">` from
136
+ `@spark-web/text`.
137
+
138
+ ## Manual sort state
139
+
140
+ When `manualSorting` is `true`, the parent owns sort state and must pass it back
141
+ to `DataTable` via `state.sorting`. Wire it up like this:
142
+
143
+ ```tsx
144
+ const [sorting, setSorting] = useState<SortingState>([]);
145
+
146
+ <DataTable
147
+ items={data}
148
+ columns={columns}
149
+ enableSorting
150
+ manualSorting
151
+ state={{ sorting }}
152
+ onSortingChange={setSorting}
153
+ />;
154
+ ```
155
+
156
+ - `enableSorting` — enables the sort UI on column headers
157
+ - `manualSorting` — tells TanStack Table not to sort `items` itself; the parent
158
+ must re-fetch/re-sort when `onSortingChange` fires
159
+ - `onSortingChange` — receives the new `SortingState`; pass it to your query as
160
+ an `orderBy` parameter
161
+ - `state.sorting` — controlled sort state, keeps headers in sync with data
162
+
163
+ To make a specific column sortable, set `enableSorting: true` on its
164
+ `ColumnDef`. To disable sorting on a specific column, set
165
+ `enableSorting: false`.
166
+
167
+ ## Spinner overlay (external loading, before table mounts)
168
+
169
+ Use `DataTable`'s `isLoading` prop for in-table loading. Only use an external
170
+ Spinner overlay when the table cannot mount yet — e.g. a full-region loading
171
+ state before you have any data at all, or a loading state that covers the entire
172
+ page section including the filter bar:
173
+
174
+ ```tsx
175
+ {
176
+ isLoading ? (
177
+ <Box
178
+ display="flex"
179
+ flexDirection="column"
180
+ alignItems="center"
181
+ justifyContent="center"
182
+ gap="small"
183
+ padding="xlarge"
184
+ >
185
+ <Spinner tone="primary" size="xsmall" />
186
+ <Text tone="muted">Loading</Text>
187
+ </Box>
188
+ ) : (
189
+ <DataTable items={data} columns={columns} isLoading={isFetching} />
190
+ );
191
+ }
192
+ ```
193
+
194
+ The distinction:
195
+
196
+ - `isLoading` (initial, no table yet) → external Spinner overlay
197
+ - `isFetching` (refetch while table exists) → pass to `DataTable` `isLoading`
198
+
199
+ ## Do NOTs
200
+
201
+ - NEVER raw hex values — always use theme color tokens
202
+ - NEVER raw pixel values — always use theme spacing/sizing tokens
203
+ - NEVER Tailwind classes
204
+ - NEVER `ChevronUp/DownIcon` for sort — use `SortAscending/DescendingIcon`
205
+ - NEVER `primaryMuted` for selected rows — use `primarySoft`
206
+ - NEVER `neutralHover` for row hover — use `color.background.inputPressed`
207
+ - NEVER apply row hover to a non-clickable row — only rows with
208
+ `enableClickableRow` get hover
209
+ - NEVER let row hover fire when the cursor is over a button inside the row — use
210
+ `:hover:not(:has(button:hover))` to suppress it
211
+ - NEVER put pagination inside `DataTable` — caller handles it externally
212
+ - NEVER render an external `<Spinner>` alongside a mounted `DataTable` — use the
213
+ `isLoading` prop instead. An external Spinner is only correct before the table
214
+ can mount (initial page load with no data yet).
215
+ - NEVER pass a string class from `@emotion/css` to `className` or
216
+ `headerClassName` — these props expect `SerializedStyles` from
217
+ `@emotion/react`'s `css` tagged template
@@ -2,13 +2,13 @@ import type { SerializedStyles } from '@emotion/react';
2
2
  import { type DataAttributeMap } from '@spark-web/utils/internal';
3
3
  import type { ColumnDef, ColumnHelper, ExpandedState, OnChangeFn, Row, SortingState, Table, TableOptions } from '@tanstack/react-table';
4
4
  import { createColumnHelper, flexRender } from '@tanstack/react-table';
5
- import type { ReactElement } from 'react';
5
+ import type { ReactElement, ReactNode } from 'react';
6
6
  /**
7
7
  * DataTable
8
8
  *
9
9
  * @see https://spark.brighte.com.au/package/data-table
10
10
  */
11
- declare function DataTable<T>({ data, items, className, columns, enableExpanding, enableHiding, enableMultiRowSelection, enableClickableRow, headerClassName, footerClassName, showBottomSpinner: showSpinner, rowClassName, expandedRowComponent, onBottomSpinnerShown, onRowClick, onRowSelectionChange, renderRow, ...tableOptions }: DataTableProps<T>): import("@emotion/react/jsx-runtime").JSX.Element;
11
+ declare function DataTable<T>({ data, items, className, columns, enableExpanding, enableHiding, enableMultiRowSelection, enableClickableRow, headerClassName, footerClassName, showBottomSpinner: showSpinner, rowClassName, expandedRowComponent, onBottomSpinnerShown, onRowClick, onRowSelectionChange, renderRow, isLoading, emptyState, ...tableOptions }: DataTableProps<T>): import("@emotion/react/jsx-runtime").JSX.Element;
12
12
  type TableSubsetOptions<T> = Pick<TableOptions<T>, 'columns' | 'enableExpanding' | 'enableHiding' | 'enableMultiRowSelection' | 'state' | 'onStateChange' | 'onExpandedChange' | 'onRowSelectionChange' | 'getRowId' | 'enableSorting' | 'manualSorting'>;
13
13
  type DataTableProps<T> = TableSubsetOptions<T> & {
14
14
  className?: SerializedStyles;
@@ -25,6 +25,10 @@ type DataTableProps<T> = TableSubsetOptions<T> & {
25
25
  onRowClick?: (row: Row<T>) => void;
26
26
  renderRow?: (table: Table<T>) => ReactElement<T>;
27
27
  onSortingChange?: OnChangeFn<SortingState>;
28
+ /** When true, renders a centered spinner + "Loading" text below the table. */
29
+ isLoading?: boolean;
30
+ /** Rendered below the table when not loading and items is empty. */
31
+ emptyState?: ReactNode;
28
32
  };
29
33
  export { createColumnHelper, DataTable, flexRender };
30
34
  export type { ColumnDef, ColumnHelper, DataTableProps, Row as DataTableRow, ExpandedState, OnChangeFn, };
@@ -8,6 +8,7 @@ var react = require('@emotion/react');
8
8
  var box = require('@spark-web/box');
9
9
  var icon = require('@spark-web/icon');
10
10
  var spinner = require('@spark-web/spinner');
11
+ var text = require('@spark-web/text');
11
12
  var theme = require('@spark-web/theme');
12
13
  var internal = require('@spark-web/utils/internal');
13
14
  var reactTable = require('@tanstack/react-table');
@@ -19,7 +20,22 @@ function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e };
19
20
 
20
21
  var React__default = /*#__PURE__*/_interopDefault(React);
21
22
 
22
- var _excluded = ["data", "items", "className", "columns", "enableExpanding", "enableHiding", "enableMultiRowSelection", "enableClickableRow", "headerClassName", "footerClassName", "showBottomSpinner", "rowClassName", "expandedRowComponent", "onBottomSpinnerShown", "onRowClick", "onRowSelectionChange", "renderRow"];
23
+ var _excluded = ["data", "items", "className", "columns", "enableExpanding", "enableHiding", "enableMultiRowSelection", "enableClickableRow", "headerClassName", "footerClassName", "showBottomSpinner", "rowClassName", "expandedRowComponent", "onBottomSpinnerShown", "onRowClick", "onRowSelectionChange", "renderRow", "isLoading", "emptyState"];
24
+ function getAriaSort(column) {
25
+ if (!column.getCanSort()) return undefined;
26
+ if (column.getIsSorted() === 'asc') return 'ascending';
27
+ if (column.getIsSorted() === 'desc') return 'descending';
28
+ return 'none';
29
+ }
30
+
31
+ ////////////////////////////////////////////////////////////////////////////////
32
+
33
+ /**
34
+ * DataTable
35
+ *
36
+ * @see https://spark.brighte.com.au/package/data-table
37
+ */
38
+
23
39
  function DataTable(_ref) {
24
40
  var _tableOptions$enableS, _tableOptions$manualS;
25
41
  var data = _ref.data,
@@ -42,6 +58,8 @@ function DataTable(_ref) {
42
58
  onRowClick = _ref.onRowClick,
43
59
  onRowSelectionChange = _ref.onRowSelectionChange,
44
60
  renderRow = _ref.renderRow,
61
+ isLoading = _ref.isLoading,
62
+ emptyState = _ref.emptyState,
45
63
  tableOptions = _objectWithoutProperties(_ref, _excluded);
46
64
  var theme$1 = theme.useTheme();
47
65
  var enableRowSelection = !!onRowSelectionChange;
@@ -64,130 +82,164 @@ function DataTable(_ref) {
64
82
  var defaultOnRowClick = function defaultOnRowClick(row) {
65
83
  return row.toggleSelected();
66
84
  };
67
- return jsxRuntime.jsxs(box.Box, _objectSpread(_objectSpread({
68
- as: "table"
69
- }, data ? internal.buildDataAttributes(data) : undefined), {}, {
70
- css: className,
71
- children: [headerGroups.length > 0 ? jsxRuntime.jsx(box.Box, {
72
- as: "thead",
73
- background: "neutral",
74
- css: react.css({
75
- // we use box shadow instead of bottom border
76
- // to allow the border to stay in the header on scroll
77
- // when the table is sticky
78
- boxShadow: "inset 0 -2px 0 ".concat(theme$1.border.color.primary)
79
- }, headerClassName),
80
- children: headerGroups.map(function (headerGroup) {
81
- return jsxRuntime.jsx(box.Box, {
82
- as: "tr",
83
- children: headerGroup.headers.map(function (header) {
84
- var _asc$desc;
85
- return jsxRuntime.jsxs(box.Box, {
86
- as: "th",
87
- color: theme$1.color.foreground.muted,
88
- padding: "medium",
89
- css: react.css({
90
- fontFamily: theme$1.typography.fontFamily.sans.name,
91
- fontWeight: theme$1.typography.fontWeight.semibold,
92
- textAlign: 'left',
93
- textTransform: 'uppercase',
94
- width: "".concat(header.getSize(), "px"),
95
- svg: {
96
- marginLeft: theme$1.spacing.xsmall
97
- }
98
- }),
99
- onClick: header.column.getToggleSortingHandler(),
100
- children: [header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext()), (_asc$desc = {
101
- asc: jsxRuntime.jsx(icon.ChevronUpIcon, {
102
- size: "xsmall"
103
- }),
104
- desc: jsxRuntime.jsx(icon.ChevronDownIcon, {
105
- size: "xsmall"
106
- })
107
- }[header.column.getIsSorted()]) !== null && _asc$desc !== void 0 ? _asc$desc : null]
108
- }, header.id);
109
- })
110
- }, headerGroup.id);
111
- })
112
- }) : null, jsxRuntime.jsxs(box.Box, {
113
- as: "tbody",
114
- children: [!renderRow ? table.getRowModel().rows.map(function (row) {
115
- return jsxRuntime.jsxs(React__default["default"].Fragment, {
116
- children: [jsxRuntime.jsx(box.Box, {
85
+ return jsxRuntime.jsxs(box.Box, _objectSpread(_objectSpread({}, data ? internal.buildDataAttributes(data) : undefined), {}, {
86
+ children: [jsxRuntime.jsxs(box.Box, {
87
+ as: "table",
88
+ width: "full",
89
+ css: className,
90
+ children: [headerGroups.length > 0 ? jsxRuntime.jsx(box.Box, {
91
+ as: "thead",
92
+ css: react.css({
93
+ backgroundColor: theme$1.color.background.surface,
94
+ // we use box shadow instead of bottom border
95
+ // to allow the border to stay in the header on scroll
96
+ // when the table is sticky
97
+ boxShadow: "inset 0 -2px 0 ".concat(theme$1.border.color.primaryActive)
98
+ }, headerClassName),
99
+ children: headerGroups.map(function (headerGroup) {
100
+ return jsxRuntime.jsx(box.Box, {
117
101
  as: "tr",
102
+ children: headerGroup.headers.map(function (header) {
103
+ var _asc$desc;
104
+ return jsxRuntime.jsxs(box.Box, {
105
+ as: "th",
106
+ padding: "medium",
107
+ css: react.css(_objectSpread(_objectSpread({
108
+ color: theme$1.color.foreground.primaryActive,
109
+ fontFamily: theme$1.typography.fontFamily.sans.name,
110
+ fontWeight: theme$1.typography.fontWeight.semibold,
111
+ textAlign: 'left',
112
+ width: "".concat(header.getSize(), "px"),
113
+ cursor: header.column.getCanSort() ? 'pointer' : undefined,
114
+ userSelect: header.column.getCanSort() ? 'none' : undefined
115
+ }, header.column.getCanSort() ? {
116
+ '&:hover': {
117
+ backgroundColor: theme$1.backgroundInteractions.primaryLowHover
118
+ }
119
+ } : {}), {}, {
120
+ svg: {
121
+ marginLeft: theme$1.spacing.xsmall
122
+ }
123
+ })),
124
+ onClick: header.column.getToggleSortingHandler(),
125
+ "aria-sort": getAriaSort(header.column),
126
+ children: [header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext()), (_asc$desc = {
127
+ asc: jsxRuntime.jsx(icon.SortAscendingIcon, {
128
+ size: "xxsmall",
129
+ tone: "primaryActive"
130
+ }),
131
+ desc: jsxRuntime.jsx(icon.SortDescendingIcon, {
132
+ size: "xxsmall",
133
+ tone: "primaryActive"
134
+ })
135
+ }[header.column.getIsSorted()]) !== null && _asc$desc !== void 0 ? _asc$desc : null]
136
+ }, header.id);
137
+ })
138
+ }, headerGroup.id);
139
+ })
140
+ }) : null, jsxRuntime.jsxs(box.Box, {
141
+ as: "tbody",
142
+ children: [!renderRow ? table.getRowModel().rows.map(function (row) {
143
+ return jsxRuntime.jsxs(React__default["default"].Fragment, {
144
+ children: [jsxRuntime.jsx(box.Box, {
145
+ as: "tr",
146
+ css: react.css(_objectSpread({
147
+ '&[data-is-selected="true"],&[data-is-expanded="true"]': {
148
+ backgroundColor: theme$1.color.background.primarySoft
149
+ }
150
+ }, enableClickableRow ? {
151
+ ':hover:not(:has(button:hover))': {
152
+ backgroundColor: theme$1.color.background.inputPressed,
153
+ cursor: 'pointer'
154
+ }
155
+ } : {}), rowClassName ? rowClassName(row, row.index) : undefined),
156
+ onClick: function onClick() {
157
+ return enableClickableRow && (onRowClick ? onRowClick(row) : defaultOnRowClick(row));
158
+ },
159
+ "data-is-selected": row.getIsSelected(),
160
+ "data-is-expanded": row.getIsExpanded(),
161
+ children: row.getVisibleCells().map(function (cell) {
162
+ return jsxRuntime.jsx(box.Box, {
163
+ as: "td",
164
+ padding: "large",
165
+ css: react.css({
166
+ borderBottomWidth: theme$1.border.width.standard,
167
+ borderBottomStyle: 'solid',
168
+ borderColor: theme$1.border.color.standard,
169
+ verticalAlign: 'middle',
170
+ textAlign: 'left'
171
+ }),
172
+ children: reactTable.flexRender(cell.column.columnDef.cell, cell.getContext())
173
+ }, cell.id);
174
+ })
175
+ }), enableExpanding && row.getIsExpanded() && expandedRowComponent && expandedRowComponent(row, table)]
176
+ }, row.id);
177
+ }) : renderRow(table), showSpinner && jsxRuntime.jsx(reactIntersectionObserver.InView, {
178
+ as: "tr"
179
+ // @ts-ignore
180
+ ,
181
+ css: react.css({
182
+ columnSpan: 'all'
183
+ }),
184
+ onChange: onBottomSpinnerShown,
185
+ children: jsxRuntime.jsx(box.Box, {
186
+ as: "td",
187
+ padding: "large",
188
+ colSpan: headerGroups.map(function (hg) {
189
+ return hg.headers.length;
190
+ }).reduce(function (sum, len) {
191
+ return len + 1;
192
+ }, 0),
118
193
  css: react.css({
119
- '&[data-is-selected="true"],&[data-is-expanded="true"]': {
120
- background: theme$1.color.background.primaryMuted
194
+ textAlign: 'center',
195
+ verticalAlign: 'middle'
196
+ }),
197
+ children: jsxRuntime.jsx(spinner.Spinner, {
198
+ data: {
199
+ testId: 'data-table-spinner'
121
200
  },
122
- ':hover': {
123
- background: theme$1.backgroundInteractions.neutralHover,
124
- cursor: enableClickableRow ? 'pointer' : undefined
125
- }
126
- }, rowClassName ? rowClassName(row, row.index) : undefined),
127
- onClick: function onClick() {
128
- return enableClickableRow && (onRowClick ? onRowClick(row) : defaultOnRowClick(row));
129
- },
130
- "data-is-selected": row.getIsSelected(),
131
- "data-is-expanded": row.getIsExpanded(),
132
- children: row.getVisibleCells().map(function (cell) {
201
+ size: "xsmall"
202
+ })
203
+ })
204
+ })]
205
+ }), footerGroups.length > 0 ? jsxRuntime.jsx(box.Box, {
206
+ as: "tfoot",
207
+ css: react.css(footerClassName),
208
+ children: footerGroups.map(function (footerGroup) {
209
+ return jsxRuntime.jsx(box.Box, {
210
+ as: "tr",
211
+ children: footerGroup.headers.map(function (footer) {
133
212
  return jsxRuntime.jsx(box.Box, {
134
213
  as: "td",
135
- padding: "large",
136
- css: react.css({
137
- borderBottom: '1px',
138
- borderBottomStyle: 'solid',
139
- borderColor: theme$1.border.color.standard,
140
- verticalAlign: 'middle',
141
- textAlign: 'left'
142
- }),
143
- children: reactTable.flexRender(cell.column.columnDef.cell, cell.getContext())
144
- }, cell.id);
214
+ children: reactTable.flexRender(footer.column.columnDef.footer, footer.getContext())
215
+ }, footer.id);
145
216
  })
146
- }), enableExpanding && row.getIsExpanded() && expandedRowComponent && expandedRowComponent(row, table)]
147
- }, row.id);
148
- }) : renderRow(table), showSpinner && jsxRuntime.jsx(reactIntersectionObserver.InView, {
149
- as: "tr"
150
- // @ts-ignore
151
- ,
152
- css: react.css({
153
- columnSpan: 'all'
154
- }),
155
- onChange: onBottomSpinnerShown,
156
- children: jsxRuntime.jsx(box.Box, {
157
- as: "td",
158
- padding: "large",
159
- colSpan: headerGroups.map(function (hg) {
160
- return hg.headers.length;
161
- }).reduce(function (sum, len) {
162
- return len + 1;
163
- }, 0),
164
- css: react.css({
165
- textAlign: 'center',
166
- verticalAlign: 'middle'
167
- }),
168
- children: jsxRuntime.jsx(spinner.Spinner, {
169
- data: {
170
- testId: 'data-table-spinner'
171
- },
172
- size: "xsmall"
173
- })
217
+ }, footerGroup.id);
174
218
  })
219
+ }) : null]
220
+ }), isLoading && jsxRuntime.jsxs(box.Box, {
221
+ display: "flex",
222
+ alignItems: "center",
223
+ justifyContent: "center",
224
+ padding: "xlarge",
225
+ gap: "small",
226
+ width: "full",
227
+ children: [jsxRuntime.jsx(text.Text, {
228
+ tone: "muted",
229
+ children: "Loading"
230
+ }), jsxRuntime.jsx(spinner.Spinner, {
231
+ data: {
232
+ testId: 'data-table-loading-spinner'
233
+ },
234
+ tone: "primary"
175
235
  })]
176
- }), footerGroups.length > 0 ? jsxRuntime.jsx(box.Box, {
177
- as: "tfoot",
178
- css: react.css(footerClassName),
179
- children: footerGroups.map(function (footerGroup) {
180
- return jsxRuntime.jsx(box.Box, {
181
- as: "tr",
182
- children: footerGroup.headers.map(function (footer) {
183
- return jsxRuntime.jsx(box.Box, {
184
- as: "td",
185
- children: reactTable.flexRender(footer.column.columnDef.footer, footer.getContext())
186
- }, footer.id);
187
- })
188
- }, footerGroup.id);
189
- })
190
- }) : null]
236
+ }), !isLoading && items.length === 0 && emptyState != null && jsxRuntime.jsx(box.Box, {
237
+ display: "flex",
238
+ justifyContent: "center",
239
+ padding: "xlarge",
240
+ width: "full",
241
+ children: emptyState
242
+ })]
191
243
  }));
192
244
  }
193
245
 
@@ -8,6 +8,7 @@ var react = require('@emotion/react');
8
8
  var box = require('@spark-web/box');
9
9
  var icon = require('@spark-web/icon');
10
10
  var spinner = require('@spark-web/spinner');
11
+ var text = require('@spark-web/text');
11
12
  var theme = require('@spark-web/theme');
12
13
  var internal = require('@spark-web/utils/internal');
13
14
  var reactTable = require('@tanstack/react-table');
@@ -19,7 +20,22 @@ function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e };
19
20
 
20
21
  var React__default = /*#__PURE__*/_interopDefault(React);
21
22
 
22
- var _excluded = ["data", "items", "className", "columns", "enableExpanding", "enableHiding", "enableMultiRowSelection", "enableClickableRow", "headerClassName", "footerClassName", "showBottomSpinner", "rowClassName", "expandedRowComponent", "onBottomSpinnerShown", "onRowClick", "onRowSelectionChange", "renderRow"];
23
+ var _excluded = ["data", "items", "className", "columns", "enableExpanding", "enableHiding", "enableMultiRowSelection", "enableClickableRow", "headerClassName", "footerClassName", "showBottomSpinner", "rowClassName", "expandedRowComponent", "onBottomSpinnerShown", "onRowClick", "onRowSelectionChange", "renderRow", "isLoading", "emptyState"];
24
+ function getAriaSort(column) {
25
+ if (!column.getCanSort()) return undefined;
26
+ if (column.getIsSorted() === 'asc') return 'ascending';
27
+ if (column.getIsSorted() === 'desc') return 'descending';
28
+ return 'none';
29
+ }
30
+
31
+ ////////////////////////////////////////////////////////////////////////////////
32
+
33
+ /**
34
+ * DataTable
35
+ *
36
+ * @see https://spark.brighte.com.au/package/data-table
37
+ */
38
+
23
39
  function DataTable(_ref) {
24
40
  var _tableOptions$enableS, _tableOptions$manualS;
25
41
  var data = _ref.data,
@@ -42,6 +58,8 @@ function DataTable(_ref) {
42
58
  onRowClick = _ref.onRowClick,
43
59
  onRowSelectionChange = _ref.onRowSelectionChange,
44
60
  renderRow = _ref.renderRow,
61
+ isLoading = _ref.isLoading,
62
+ emptyState = _ref.emptyState,
45
63
  tableOptions = _objectWithoutProperties(_ref, _excluded);
46
64
  var theme$1 = theme.useTheme();
47
65
  var enableRowSelection = !!onRowSelectionChange;
@@ -64,130 +82,164 @@ function DataTable(_ref) {
64
82
  var defaultOnRowClick = function defaultOnRowClick(row) {
65
83
  return row.toggleSelected();
66
84
  };
67
- return jsxRuntime.jsxs(box.Box, _objectSpread(_objectSpread({
68
- as: "table"
69
- }, data ? internal.buildDataAttributes(data) : undefined), {}, {
70
- css: className,
71
- children: [headerGroups.length > 0 ? jsxRuntime.jsx(box.Box, {
72
- as: "thead",
73
- background: "neutral",
74
- css: react.css({
75
- // we use box shadow instead of bottom border
76
- // to allow the border to stay in the header on scroll
77
- // when the table is sticky
78
- boxShadow: "inset 0 -2px 0 ".concat(theme$1.border.color.primary)
79
- }, headerClassName),
80
- children: headerGroups.map(function (headerGroup) {
81
- return jsxRuntime.jsx(box.Box, {
82
- as: "tr",
83
- children: headerGroup.headers.map(function (header) {
84
- var _asc$desc;
85
- return jsxRuntime.jsxs(box.Box, {
86
- as: "th",
87
- color: theme$1.color.foreground.muted,
88
- padding: "medium",
89
- css: react.css({
90
- fontFamily: theme$1.typography.fontFamily.sans.name,
91
- fontWeight: theme$1.typography.fontWeight.semibold,
92
- textAlign: 'left',
93
- textTransform: 'uppercase',
94
- width: "".concat(header.getSize(), "px"),
95
- svg: {
96
- marginLeft: theme$1.spacing.xsmall
97
- }
98
- }),
99
- onClick: header.column.getToggleSortingHandler(),
100
- children: [header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext()), (_asc$desc = {
101
- asc: jsxRuntime.jsx(icon.ChevronUpIcon, {
102
- size: "xsmall"
103
- }),
104
- desc: jsxRuntime.jsx(icon.ChevronDownIcon, {
105
- size: "xsmall"
106
- })
107
- }[header.column.getIsSorted()]) !== null && _asc$desc !== void 0 ? _asc$desc : null]
108
- }, header.id);
109
- })
110
- }, headerGroup.id);
111
- })
112
- }) : null, jsxRuntime.jsxs(box.Box, {
113
- as: "tbody",
114
- children: [!renderRow ? table.getRowModel().rows.map(function (row) {
115
- return jsxRuntime.jsxs(React__default["default"].Fragment, {
116
- children: [jsxRuntime.jsx(box.Box, {
85
+ return jsxRuntime.jsxs(box.Box, _objectSpread(_objectSpread({}, data ? internal.buildDataAttributes(data) : undefined), {}, {
86
+ children: [jsxRuntime.jsxs(box.Box, {
87
+ as: "table",
88
+ width: "full",
89
+ css: className,
90
+ children: [headerGroups.length > 0 ? jsxRuntime.jsx(box.Box, {
91
+ as: "thead",
92
+ css: react.css({
93
+ backgroundColor: theme$1.color.background.surface,
94
+ // we use box shadow instead of bottom border
95
+ // to allow the border to stay in the header on scroll
96
+ // when the table is sticky
97
+ boxShadow: "inset 0 -2px 0 ".concat(theme$1.border.color.primaryActive)
98
+ }, headerClassName),
99
+ children: headerGroups.map(function (headerGroup) {
100
+ return jsxRuntime.jsx(box.Box, {
117
101
  as: "tr",
102
+ children: headerGroup.headers.map(function (header) {
103
+ var _asc$desc;
104
+ return jsxRuntime.jsxs(box.Box, {
105
+ as: "th",
106
+ padding: "medium",
107
+ css: react.css(_objectSpread(_objectSpread({
108
+ color: theme$1.color.foreground.primaryActive,
109
+ fontFamily: theme$1.typography.fontFamily.sans.name,
110
+ fontWeight: theme$1.typography.fontWeight.semibold,
111
+ textAlign: 'left',
112
+ width: "".concat(header.getSize(), "px"),
113
+ cursor: header.column.getCanSort() ? 'pointer' : undefined,
114
+ userSelect: header.column.getCanSort() ? 'none' : undefined
115
+ }, header.column.getCanSort() ? {
116
+ '&:hover': {
117
+ backgroundColor: theme$1.backgroundInteractions.primaryLowHover
118
+ }
119
+ } : {}), {}, {
120
+ svg: {
121
+ marginLeft: theme$1.spacing.xsmall
122
+ }
123
+ })),
124
+ onClick: header.column.getToggleSortingHandler(),
125
+ "aria-sort": getAriaSort(header.column),
126
+ children: [header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext()), (_asc$desc = {
127
+ asc: jsxRuntime.jsx(icon.SortAscendingIcon, {
128
+ size: "xxsmall",
129
+ tone: "primaryActive"
130
+ }),
131
+ desc: jsxRuntime.jsx(icon.SortDescendingIcon, {
132
+ size: "xxsmall",
133
+ tone: "primaryActive"
134
+ })
135
+ }[header.column.getIsSorted()]) !== null && _asc$desc !== void 0 ? _asc$desc : null]
136
+ }, header.id);
137
+ })
138
+ }, headerGroup.id);
139
+ })
140
+ }) : null, jsxRuntime.jsxs(box.Box, {
141
+ as: "tbody",
142
+ children: [!renderRow ? table.getRowModel().rows.map(function (row) {
143
+ return jsxRuntime.jsxs(React__default["default"].Fragment, {
144
+ children: [jsxRuntime.jsx(box.Box, {
145
+ as: "tr",
146
+ css: react.css(_objectSpread({
147
+ '&[data-is-selected="true"],&[data-is-expanded="true"]': {
148
+ backgroundColor: theme$1.color.background.primarySoft
149
+ }
150
+ }, enableClickableRow ? {
151
+ ':hover:not(:has(button:hover))': {
152
+ backgroundColor: theme$1.color.background.inputPressed,
153
+ cursor: 'pointer'
154
+ }
155
+ } : {}), rowClassName ? rowClassName(row, row.index) : undefined),
156
+ onClick: function onClick() {
157
+ return enableClickableRow && (onRowClick ? onRowClick(row) : defaultOnRowClick(row));
158
+ },
159
+ "data-is-selected": row.getIsSelected(),
160
+ "data-is-expanded": row.getIsExpanded(),
161
+ children: row.getVisibleCells().map(function (cell) {
162
+ return jsxRuntime.jsx(box.Box, {
163
+ as: "td",
164
+ padding: "large",
165
+ css: react.css({
166
+ borderBottomWidth: theme$1.border.width.standard,
167
+ borderBottomStyle: 'solid',
168
+ borderColor: theme$1.border.color.standard,
169
+ verticalAlign: 'middle',
170
+ textAlign: 'left'
171
+ }),
172
+ children: reactTable.flexRender(cell.column.columnDef.cell, cell.getContext())
173
+ }, cell.id);
174
+ })
175
+ }), enableExpanding && row.getIsExpanded() && expandedRowComponent && expandedRowComponent(row, table)]
176
+ }, row.id);
177
+ }) : renderRow(table), showSpinner && jsxRuntime.jsx(reactIntersectionObserver.InView, {
178
+ as: "tr"
179
+ // @ts-ignore
180
+ ,
181
+ css: react.css({
182
+ columnSpan: 'all'
183
+ }),
184
+ onChange: onBottomSpinnerShown,
185
+ children: jsxRuntime.jsx(box.Box, {
186
+ as: "td",
187
+ padding: "large",
188
+ colSpan: headerGroups.map(function (hg) {
189
+ return hg.headers.length;
190
+ }).reduce(function (sum, len) {
191
+ return len + 1;
192
+ }, 0),
118
193
  css: react.css({
119
- '&[data-is-selected="true"],&[data-is-expanded="true"]': {
120
- background: theme$1.color.background.primaryMuted
194
+ textAlign: 'center',
195
+ verticalAlign: 'middle'
196
+ }),
197
+ children: jsxRuntime.jsx(spinner.Spinner, {
198
+ data: {
199
+ testId: 'data-table-spinner'
121
200
  },
122
- ':hover': {
123
- background: theme$1.backgroundInteractions.neutralHover,
124
- cursor: enableClickableRow ? 'pointer' : undefined
125
- }
126
- }, rowClassName ? rowClassName(row, row.index) : undefined),
127
- onClick: function onClick() {
128
- return enableClickableRow && (onRowClick ? onRowClick(row) : defaultOnRowClick(row));
129
- },
130
- "data-is-selected": row.getIsSelected(),
131
- "data-is-expanded": row.getIsExpanded(),
132
- children: row.getVisibleCells().map(function (cell) {
201
+ size: "xsmall"
202
+ })
203
+ })
204
+ })]
205
+ }), footerGroups.length > 0 ? jsxRuntime.jsx(box.Box, {
206
+ as: "tfoot",
207
+ css: react.css(footerClassName),
208
+ children: footerGroups.map(function (footerGroup) {
209
+ return jsxRuntime.jsx(box.Box, {
210
+ as: "tr",
211
+ children: footerGroup.headers.map(function (footer) {
133
212
  return jsxRuntime.jsx(box.Box, {
134
213
  as: "td",
135
- padding: "large",
136
- css: react.css({
137
- borderBottom: '1px',
138
- borderBottomStyle: 'solid',
139
- borderColor: theme$1.border.color.standard,
140
- verticalAlign: 'middle',
141
- textAlign: 'left'
142
- }),
143
- children: reactTable.flexRender(cell.column.columnDef.cell, cell.getContext())
144
- }, cell.id);
214
+ children: reactTable.flexRender(footer.column.columnDef.footer, footer.getContext())
215
+ }, footer.id);
145
216
  })
146
- }), enableExpanding && row.getIsExpanded() && expandedRowComponent && expandedRowComponent(row, table)]
147
- }, row.id);
148
- }) : renderRow(table), showSpinner && jsxRuntime.jsx(reactIntersectionObserver.InView, {
149
- as: "tr"
150
- // @ts-ignore
151
- ,
152
- css: react.css({
153
- columnSpan: 'all'
154
- }),
155
- onChange: onBottomSpinnerShown,
156
- children: jsxRuntime.jsx(box.Box, {
157
- as: "td",
158
- padding: "large",
159
- colSpan: headerGroups.map(function (hg) {
160
- return hg.headers.length;
161
- }).reduce(function (sum, len) {
162
- return len + 1;
163
- }, 0),
164
- css: react.css({
165
- textAlign: 'center',
166
- verticalAlign: 'middle'
167
- }),
168
- children: jsxRuntime.jsx(spinner.Spinner, {
169
- data: {
170
- testId: 'data-table-spinner'
171
- },
172
- size: "xsmall"
173
- })
217
+ }, footerGroup.id);
174
218
  })
219
+ }) : null]
220
+ }), isLoading && jsxRuntime.jsxs(box.Box, {
221
+ display: "flex",
222
+ alignItems: "center",
223
+ justifyContent: "center",
224
+ padding: "xlarge",
225
+ gap: "small",
226
+ width: "full",
227
+ children: [jsxRuntime.jsx(text.Text, {
228
+ tone: "muted",
229
+ children: "Loading"
230
+ }), jsxRuntime.jsx(spinner.Spinner, {
231
+ data: {
232
+ testId: 'data-table-loading-spinner'
233
+ },
234
+ tone: "primary"
175
235
  })]
176
- }), footerGroups.length > 0 ? jsxRuntime.jsx(box.Box, {
177
- as: "tfoot",
178
- css: react.css(footerClassName),
179
- children: footerGroups.map(function (footerGroup) {
180
- return jsxRuntime.jsx(box.Box, {
181
- as: "tr",
182
- children: footerGroup.headers.map(function (footer) {
183
- return jsxRuntime.jsx(box.Box, {
184
- as: "td",
185
- children: reactTable.flexRender(footer.column.columnDef.footer, footer.getContext())
186
- }, footer.id);
187
- })
188
- }, footerGroup.id);
189
- })
190
- }) : null]
236
+ }), !isLoading && items.length === 0 && emptyState != null && jsxRuntime.jsx(box.Box, {
237
+ display: "flex",
238
+ justifyContent: "center",
239
+ padding: "xlarge",
240
+ width: "full",
241
+ children: emptyState
242
+ })]
191
243
  }));
192
244
  }
193
245
 
@@ -2,8 +2,9 @@ import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2';
2
2
  import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties';
3
3
  import { css } from '@emotion/react';
4
4
  import { Box } from '@spark-web/box';
5
- import { ChevronUpIcon, ChevronDownIcon } from '@spark-web/icon';
5
+ import { SortAscendingIcon, SortDescendingIcon } from '@spark-web/icon';
6
6
  import { Spinner } from '@spark-web/spinner';
7
+ import { Text } from '@spark-web/text';
7
8
  import { useTheme } from '@spark-web/theme';
8
9
  import { buildDataAttributes } from '@spark-web/utils/internal';
9
10
  import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
@@ -12,7 +13,22 @@ import React from 'react';
12
13
  import { InView } from 'react-intersection-observer';
13
14
  import { jsxs, jsx } from '@emotion/react/jsx-runtime';
14
15
 
15
- var _excluded = ["data", "items", "className", "columns", "enableExpanding", "enableHiding", "enableMultiRowSelection", "enableClickableRow", "headerClassName", "footerClassName", "showBottomSpinner", "rowClassName", "expandedRowComponent", "onBottomSpinnerShown", "onRowClick", "onRowSelectionChange", "renderRow"];
16
+ var _excluded = ["data", "items", "className", "columns", "enableExpanding", "enableHiding", "enableMultiRowSelection", "enableClickableRow", "headerClassName", "footerClassName", "showBottomSpinner", "rowClassName", "expandedRowComponent", "onBottomSpinnerShown", "onRowClick", "onRowSelectionChange", "renderRow", "isLoading", "emptyState"];
17
+ function getAriaSort(column) {
18
+ if (!column.getCanSort()) return undefined;
19
+ if (column.getIsSorted() === 'asc') return 'ascending';
20
+ if (column.getIsSorted() === 'desc') return 'descending';
21
+ return 'none';
22
+ }
23
+
24
+ ////////////////////////////////////////////////////////////////////////////////
25
+
26
+ /**
27
+ * DataTable
28
+ *
29
+ * @see https://spark.brighte.com.au/package/data-table
30
+ */
31
+
16
32
  function DataTable(_ref) {
17
33
  var _tableOptions$enableS, _tableOptions$manualS;
18
34
  var data = _ref.data,
@@ -35,6 +51,8 @@ function DataTable(_ref) {
35
51
  onRowClick = _ref.onRowClick,
36
52
  onRowSelectionChange = _ref.onRowSelectionChange,
37
53
  renderRow = _ref.renderRow,
54
+ isLoading = _ref.isLoading,
55
+ emptyState = _ref.emptyState,
38
56
  tableOptions = _objectWithoutProperties(_ref, _excluded);
39
57
  var theme = useTheme();
40
58
  var enableRowSelection = !!onRowSelectionChange;
@@ -57,130 +75,164 @@ function DataTable(_ref) {
57
75
  var defaultOnRowClick = function defaultOnRowClick(row) {
58
76
  return row.toggleSelected();
59
77
  };
60
- return jsxs(Box, _objectSpread(_objectSpread({
61
- as: "table"
62
- }, data ? buildDataAttributes(data) : undefined), {}, {
63
- css: className,
64
- children: [headerGroups.length > 0 ? jsx(Box, {
65
- as: "thead",
66
- background: "neutral",
67
- css: css({
68
- // we use box shadow instead of bottom border
69
- // to allow the border to stay in the header on scroll
70
- // when the table is sticky
71
- boxShadow: "inset 0 -2px 0 ".concat(theme.border.color.primary)
72
- }, headerClassName),
73
- children: headerGroups.map(function (headerGroup) {
74
- return jsx(Box, {
75
- as: "tr",
76
- children: headerGroup.headers.map(function (header) {
77
- var _asc$desc;
78
- return jsxs(Box, {
79
- as: "th",
80
- color: theme.color.foreground.muted,
81
- padding: "medium",
82
- css: css({
83
- fontFamily: theme.typography.fontFamily.sans.name,
84
- fontWeight: theme.typography.fontWeight.semibold,
85
- textAlign: 'left',
86
- textTransform: 'uppercase',
87
- width: "".concat(header.getSize(), "px"),
88
- svg: {
89
- marginLeft: theme.spacing.xsmall
90
- }
91
- }),
92
- onClick: header.column.getToggleSortingHandler(),
93
- children: [header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()), (_asc$desc = {
94
- asc: jsx(ChevronUpIcon, {
95
- size: "xsmall"
96
- }),
97
- desc: jsx(ChevronDownIcon, {
98
- size: "xsmall"
99
- })
100
- }[header.column.getIsSorted()]) !== null && _asc$desc !== void 0 ? _asc$desc : null]
101
- }, header.id);
102
- })
103
- }, headerGroup.id);
104
- })
105
- }) : null, jsxs(Box, {
106
- as: "tbody",
107
- children: [!renderRow ? table.getRowModel().rows.map(function (row) {
108
- return jsxs(React.Fragment, {
109
- children: [jsx(Box, {
78
+ return jsxs(Box, _objectSpread(_objectSpread({}, data ? buildDataAttributes(data) : undefined), {}, {
79
+ children: [jsxs(Box, {
80
+ as: "table",
81
+ width: "full",
82
+ css: className,
83
+ children: [headerGroups.length > 0 ? jsx(Box, {
84
+ as: "thead",
85
+ css: css({
86
+ backgroundColor: theme.color.background.surface,
87
+ // we use box shadow instead of bottom border
88
+ // to allow the border to stay in the header on scroll
89
+ // when the table is sticky
90
+ boxShadow: "inset 0 -2px 0 ".concat(theme.border.color.primaryActive)
91
+ }, headerClassName),
92
+ children: headerGroups.map(function (headerGroup) {
93
+ return jsx(Box, {
110
94
  as: "tr",
95
+ children: headerGroup.headers.map(function (header) {
96
+ var _asc$desc;
97
+ return jsxs(Box, {
98
+ as: "th",
99
+ padding: "medium",
100
+ css: css(_objectSpread(_objectSpread({
101
+ color: theme.color.foreground.primaryActive,
102
+ fontFamily: theme.typography.fontFamily.sans.name,
103
+ fontWeight: theme.typography.fontWeight.semibold,
104
+ textAlign: 'left',
105
+ width: "".concat(header.getSize(), "px"),
106
+ cursor: header.column.getCanSort() ? 'pointer' : undefined,
107
+ userSelect: header.column.getCanSort() ? 'none' : undefined
108
+ }, header.column.getCanSort() ? {
109
+ '&:hover': {
110
+ backgroundColor: theme.backgroundInteractions.primaryLowHover
111
+ }
112
+ } : {}), {}, {
113
+ svg: {
114
+ marginLeft: theme.spacing.xsmall
115
+ }
116
+ })),
117
+ onClick: header.column.getToggleSortingHandler(),
118
+ "aria-sort": getAriaSort(header.column),
119
+ children: [header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()), (_asc$desc = {
120
+ asc: jsx(SortAscendingIcon, {
121
+ size: "xxsmall",
122
+ tone: "primaryActive"
123
+ }),
124
+ desc: jsx(SortDescendingIcon, {
125
+ size: "xxsmall",
126
+ tone: "primaryActive"
127
+ })
128
+ }[header.column.getIsSorted()]) !== null && _asc$desc !== void 0 ? _asc$desc : null]
129
+ }, header.id);
130
+ })
131
+ }, headerGroup.id);
132
+ })
133
+ }) : null, jsxs(Box, {
134
+ as: "tbody",
135
+ children: [!renderRow ? table.getRowModel().rows.map(function (row) {
136
+ return jsxs(React.Fragment, {
137
+ children: [jsx(Box, {
138
+ as: "tr",
139
+ css: css(_objectSpread({
140
+ '&[data-is-selected="true"],&[data-is-expanded="true"]': {
141
+ backgroundColor: theme.color.background.primarySoft
142
+ }
143
+ }, enableClickableRow ? {
144
+ ':hover:not(:has(button:hover))': {
145
+ backgroundColor: theme.color.background.inputPressed,
146
+ cursor: 'pointer'
147
+ }
148
+ } : {}), rowClassName ? rowClassName(row, row.index) : undefined),
149
+ onClick: function onClick() {
150
+ return enableClickableRow && (onRowClick ? onRowClick(row) : defaultOnRowClick(row));
151
+ },
152
+ "data-is-selected": row.getIsSelected(),
153
+ "data-is-expanded": row.getIsExpanded(),
154
+ children: row.getVisibleCells().map(function (cell) {
155
+ return jsx(Box, {
156
+ as: "td",
157
+ padding: "large",
158
+ css: css({
159
+ borderBottomWidth: theme.border.width.standard,
160
+ borderBottomStyle: 'solid',
161
+ borderColor: theme.border.color.standard,
162
+ verticalAlign: 'middle',
163
+ textAlign: 'left'
164
+ }),
165
+ children: flexRender(cell.column.columnDef.cell, cell.getContext())
166
+ }, cell.id);
167
+ })
168
+ }), enableExpanding && row.getIsExpanded() && expandedRowComponent && expandedRowComponent(row, table)]
169
+ }, row.id);
170
+ }) : renderRow(table), showSpinner && jsx(InView, {
171
+ as: "tr"
172
+ // @ts-ignore
173
+ ,
174
+ css: css({
175
+ columnSpan: 'all'
176
+ }),
177
+ onChange: onBottomSpinnerShown,
178
+ children: jsx(Box, {
179
+ as: "td",
180
+ padding: "large",
181
+ colSpan: headerGroups.map(function (hg) {
182
+ return hg.headers.length;
183
+ }).reduce(function (sum, len) {
184
+ return len + 1;
185
+ }, 0),
111
186
  css: css({
112
- '&[data-is-selected="true"],&[data-is-expanded="true"]': {
113
- background: theme.color.background.primaryMuted
187
+ textAlign: 'center',
188
+ verticalAlign: 'middle'
189
+ }),
190
+ children: jsx(Spinner, {
191
+ data: {
192
+ testId: 'data-table-spinner'
114
193
  },
115
- ':hover': {
116
- background: theme.backgroundInteractions.neutralHover,
117
- cursor: enableClickableRow ? 'pointer' : undefined
118
- }
119
- }, rowClassName ? rowClassName(row, row.index) : undefined),
120
- onClick: function onClick() {
121
- return enableClickableRow && (onRowClick ? onRowClick(row) : defaultOnRowClick(row));
122
- },
123
- "data-is-selected": row.getIsSelected(),
124
- "data-is-expanded": row.getIsExpanded(),
125
- children: row.getVisibleCells().map(function (cell) {
194
+ size: "xsmall"
195
+ })
196
+ })
197
+ })]
198
+ }), footerGroups.length > 0 ? jsx(Box, {
199
+ as: "tfoot",
200
+ css: css(footerClassName),
201
+ children: footerGroups.map(function (footerGroup) {
202
+ return jsx(Box, {
203
+ as: "tr",
204
+ children: footerGroup.headers.map(function (footer) {
126
205
  return jsx(Box, {
127
206
  as: "td",
128
- padding: "large",
129
- css: css({
130
- borderBottom: '1px',
131
- borderBottomStyle: 'solid',
132
- borderColor: theme.border.color.standard,
133
- verticalAlign: 'middle',
134
- textAlign: 'left'
135
- }),
136
- children: flexRender(cell.column.columnDef.cell, cell.getContext())
137
- }, cell.id);
207
+ children: flexRender(footer.column.columnDef.footer, footer.getContext())
208
+ }, footer.id);
138
209
  })
139
- }), enableExpanding && row.getIsExpanded() && expandedRowComponent && expandedRowComponent(row, table)]
140
- }, row.id);
141
- }) : renderRow(table), showSpinner && jsx(InView, {
142
- as: "tr"
143
- // @ts-ignore
144
- ,
145
- css: css({
146
- columnSpan: 'all'
147
- }),
148
- onChange: onBottomSpinnerShown,
149
- children: jsx(Box, {
150
- as: "td",
151
- padding: "large",
152
- colSpan: headerGroups.map(function (hg) {
153
- return hg.headers.length;
154
- }).reduce(function (sum, len) {
155
- return len + 1;
156
- }, 0),
157
- css: css({
158
- textAlign: 'center',
159
- verticalAlign: 'middle'
160
- }),
161
- children: jsx(Spinner, {
162
- data: {
163
- testId: 'data-table-spinner'
164
- },
165
- size: "xsmall"
166
- })
210
+ }, footerGroup.id);
167
211
  })
212
+ }) : null]
213
+ }), isLoading && jsxs(Box, {
214
+ display: "flex",
215
+ alignItems: "center",
216
+ justifyContent: "center",
217
+ padding: "xlarge",
218
+ gap: "small",
219
+ width: "full",
220
+ children: [jsx(Text, {
221
+ tone: "muted",
222
+ children: "Loading"
223
+ }), jsx(Spinner, {
224
+ data: {
225
+ testId: 'data-table-loading-spinner'
226
+ },
227
+ tone: "primary"
168
228
  })]
169
- }), footerGroups.length > 0 ? jsx(Box, {
170
- as: "tfoot",
171
- css: css(footerClassName),
172
- children: footerGroups.map(function (footerGroup) {
173
- return jsx(Box, {
174
- as: "tr",
175
- children: footerGroup.headers.map(function (footer) {
176
- return jsx(Box, {
177
- as: "td",
178
- children: flexRender(footer.column.columnDef.footer, footer.getContext())
179
- }, footer.id);
180
- })
181
- }, footerGroup.id);
182
- })
183
- }) : null]
229
+ }), !isLoading && items.length === 0 && emptyState != null && jsx(Box, {
230
+ display: "flex",
231
+ justifyContent: "center",
232
+ padding: "xlarge",
233
+ width: "full",
234
+ children: emptyState
235
+ })]
184
236
  }));
185
237
  }
186
238
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-web/data-table",
3
- "version": "5.2.0",
3
+ "version": "5.2.1",
4
4
  "homepage": "https://github.com/brighte-labs/spark-web#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,6 +12,7 @@
12
12
  "module": "dist/spark-web-data-table.esm.js",
13
13
  "files": [
14
14
  "CHANGELOG.md",
15
+ "CLAUDE.md",
15
16
  "dist",
16
17
  "README.md"
17
18
  ],
@@ -19,11 +20,11 @@
19
20
  "@babel/runtime": "^7.25.0",
20
21
  "@emotion/react": "^11.14.0",
21
22
  "@spark-web/a11y": "^5.3.0",
22
- "@spark-web/box": "^6.0.0",
23
+ "@spark-web/box": "^6.0.1",
23
24
  "@spark-web/checkbox": "^5.1.0",
24
25
  "@spark-web/icon": "^5.1.0",
25
- "@spark-web/spinner": "^5.1.0",
26
- "@spark-web/text": "^5.3.0",
26
+ "@spark-web/spinner": "^5.1.1",
27
+ "@spark-web/text": "^5.3.1",
27
28
  "@spark-web/theme": "^5.13.0",
28
29
  "@spark-web/utils": "^5.1.0",
29
30
  "@tanstack/react-table": "^8.14.0",