@spark-web/data-table 5.2.0 → 5.2.2

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,68 @@
1
1
  # @spark-web/data-table
2
2
 
3
+ ## 5.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#798](https://github.com/brighte-labs/spark-web/pull/798)
8
+ [`a8425fd`](https://github.com/brighte-labs/spark-web/commit/a8425fd4c279ce5df76467b40aa114c14ef5e069)
9
+ Thanks [@jacobporci-brighte](https://github.com/jacobporci-brighte)! -
10
+ `DataTable` now automatically zeroes the `<td>` padding on cells whose direct
11
+ child is an `<a>`, so consumers using the row-as-link pattern can let the link
12
+ itself provide the cell padding. Without this, the cell's 16px padding ring
13
+ was unclickable. Nested anchors (deeper than direct child) are unaffected. See
14
+ the package's `CLAUDE.md` for the full pattern.
15
+
16
+ ## 5.2.1
17
+
18
+ ### Patch Changes
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
+ **docs:** add CLAUDE.md AI context files to foundation and form packages;
24
+ patch data-table with manual sort and spinner overlay patterns
25
+
26
+ - [#782](https://github.com/brighte-labs/spark-web/pull/782)
27
+ [`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)
28
+ Thanks [@jacobporci-brighte](https://github.com/jacobporci-brighte)! -
29
+ **data-table:** add `isLoading` and `emptyState` props — renders a spinner +
30
+ "Loading" text or a custom empty node below the table; table now fills its
31
+ container width by default (`width: 100%`)
32
+
33
+ - [#782](https://github.com/brighte-labs/spark-web/pull/782)
34
+ [`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)
35
+ Thanks [@jacobporci-brighte](https://github.com/jacobporci-brighte)! -
36
+ **data-table:** Fix token gaps in CLAUDE.md (header cell padding, font-family,
37
+ loading/empty state containers, sort icon margin); add Composition section;
38
+ clarify row hover only applies to clickable rows and must be suppressed when
39
+ hovering a button inside the row
40
+
41
+ **design-system:** Update internal-admin surface rules with MeatballMenu hover
42
+ suppression rule; fix list-page.md MultiSelect label pattern (gap and text
43
+ size were contradicting the rules text); update pattern file code examples to
44
+ match
45
+
46
+ **meatball-menu:** Rewrite CLAUDE.md with full structure — What this is NOT,
47
+ complete token usage table, Composition section, and Do NOTs
48
+
49
+ **status-badge:** Rewrite CLAUDE.md with full structure — What this is NOT,
50
+ complete token usage table with per-tone background mapping, Composition
51
+ section, and Do NOTs
52
+
53
+ - [#782](https://github.com/brighte-labs/spark-web/pull/782)
54
+ [`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)
55
+ Thanks [@jacobporci-brighte](https://github.com/jacobporci-brighte)! -
56
+ **data-table:** align color tokens with @spark-web/table conventions — use
57
+ `primarySoft` for selected rows, `inputPressed` for hover, `primaryActive` for
58
+ header text/border; switch sort icons to `SortAscending/DescendingIcon`; add
59
+ `aria-sort` accessibility attribute; add CLAUDE.md AI context
60
+ - Updated dependencies
61
+ [[`bb41800`](https://github.com/brighte-labs/spark-web/commit/bb418004d21165f72f4bf2456afea844ee04a21c)]:
62
+ - @spark-web/box@6.0.1
63
+ - @spark-web/spinner@5.1.1
64
+ - @spark-web/text@5.3.1
65
+
3
66
  ## 5.2.0
4
67
 
5
68
  ### Minor Changes
package/CLAUDE.md ADDED
@@ -0,0 +1,262 @@
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
+ - **Padding auto-zero for link cells**: when a cell's direct child is an `<a>`
84
+ (the "row-as-link" pattern), `DataTable` automatically zeroes the `<td>`
85
+ padding via `&:has(> a)`. The link is expected to provide the visible padding
86
+ itself, which extends the clickable area over the cell's would-be padding
87
+ ring. Nested anchors (`<a>` deeper than direct child) do NOT trigger the rule.
88
+ See "Row-as-link navigation" below.
89
+
90
+ ### Row-as-link navigation
91
+
92
+ When a row's purpose is to navigate to a URL, `onRowClick` is the wrong tool —
93
+ it gives the row no `href`, so right-click → "Open link in new tab",
94
+ cmd/ctrl-click, and middle-click don't work.
95
+
96
+ Use anchor semantics instead:
97
+
98
+ - Wrap each cell's content in your router's `<Link>` component (TanStack Router,
99
+ React Router, Next.js, etc.) with the row's destination. The `<Link>` must be
100
+ the **direct child** of the cell so the auto-padding-zero rule (see "Cell")
101
+ picks it up.
102
+ - Style the link as a block that supplies the cell padding itself:
103
+ `display: block; padding: {theme.spacing.large}; color: inherit; text-decoration: none;`.
104
+ With the `<td>`'s built-in padding zeroed out under the `:has(> a)` rule and
105
+ the link's own padding filling that space, the link covers the entire cell —
106
+ including what would have been the dead 16px padding ring — so clicks anywhere
107
+ in the cell hit the anchor. Row height and text wrapping stay identical to the
108
+ default layout. Do NOT add `width: 100%` / `box-sizing: border-box` / negative
109
+ margins: any of these alters the cell's effective content width and causes
110
+ visible row-spacing or wrap differences.
111
+ - Make only the **first** cell's link keyboard-focusable (`tabIndex={0}`); set
112
+ all other cell links to `tabIndex={-1}` so one row = one tab stop. N cells per
113
+ row should not produce N tab stops to the same destination.
114
+ - Action-only cells (meatball menus, inline buttons) must NOT be wrapped in the
115
+ row link. Since they don't render an `<a>` as a direct child, the
116
+ auto-padding-zero rule won't fire on them and they keep their normal padding
117
+ (unless you've separately zeroed the action column via a `className` rule).
118
+ - Drop `onRowClick` and `enableClickableRow` for that table — the anchors take
119
+ over navigation. Replicate the hover affordance with a `rowClassName` that
120
+ applies `theme.color.background.inputPressed` on hover (mirroring the
121
+ `:hover:not(:has(button:hover))` rule documented in "Row states").
122
+
123
+ Reference implementation: `apps/admin-portal/src/components/RowLink` in the
124
+ portal-hub repo (uses TanStack Router).
125
+
126
+ ### Sort icons
127
+
128
+ Use `SortAscendingIcon` / `SortDescendingIcon` from `@spark-web/icon` at
129
+ `size="xxsmall"` `tone="primaryActive"`. Never `ChevronUpIcon` /
130
+ `ChevronDownIcon` for sort state.
131
+
132
+ - Icon left margin: `theme.spacing.xsmall` (inline CSS on the `svg` selector)
133
+
134
+ ### Loading state (`isLoading`)
135
+
136
+ Renders below the table body when `isLoading` is `true`. Does not overlay the
137
+ table — replaces visible data feedback.
138
+
139
+ - Container padding: `padding="xlarge"` (Box prop)
140
+ - Container gap: `gap="small"` (Box prop)
141
+ - Container alignment: `alignItems="center"`, `justifyContent="center"` (Box
142
+ props)
143
+ - Text: `<Text tone="muted">Loading</Text>`
144
+ - Spinner: `<Spinner tone="primary" />` — no explicit size; uses Spinner default
145
+
146
+ ### Empty state (`emptyState`)
147
+
148
+ Renders below the table body when `isLoading` is `false` and `items` is empty.
149
+
150
+ - Container padding: `padding="xlarge"` (Box prop)
151
+ - Container alignment: `justifyContent="center"` (Box prop)
152
+ - Content: caller-provided `ReactNode` — always pass
153
+ `<Text tone="muted" size="small">…</Text>`
154
+
155
+ ## Composition
156
+
157
+ - `Box` from `@spark-web/box` — outer wrapper, `<table>`, `<thead>`, `<tbody>`,
158
+ `<tfoot>`, `<tr>`, `<th>`, `<td>` elements
159
+ - `SortAscendingIcon` / `SortDescendingIcon` from `@spark-web/icon` — sort state
160
+ icons in sortable header cells
161
+ - `Spinner` from `@spark-web/spinner` — loading overlay (via `isLoading`) and
162
+ infinite-scroll bottom spinner (via `showBottomSpinner`)
163
+ - `Text` from `@spark-web/text` — "Loading" label in `isLoading` state
164
+ - `InView` from `react-intersection-observer` — triggers `onBottomSpinnerShown`
165
+ when the bottom spinner enters the viewport
166
+
167
+ ## Column definitions
168
+
169
+ Use `createColumnHelper<T>()` from this package for type-safe column defs:
170
+
171
+ ```tsx
172
+ import { createColumnHelper } from '@spark-web/data-table';
173
+ const col = createColumnHelper<MyType>();
174
+ ```
175
+
176
+ Cell renderers receive `info` (`CellContext`) — use `info.getValue()` or
177
+ `info.renderValue()`. Wrap cell content in `<Text size="small">` from
178
+ `@spark-web/text`.
179
+
180
+ ## Manual sort state
181
+
182
+ When `manualSorting` is `true`, the parent owns sort state and must pass it back
183
+ to `DataTable` via `state.sorting`. Wire it up like this:
184
+
185
+ ```tsx
186
+ const [sorting, setSorting] = useState<SortingState>([]);
187
+
188
+ <DataTable
189
+ items={data}
190
+ columns={columns}
191
+ enableSorting
192
+ manualSorting
193
+ state={{ sorting }}
194
+ onSortingChange={setSorting}
195
+ />;
196
+ ```
197
+
198
+ - `enableSorting` — enables the sort UI on column headers
199
+ - `manualSorting` — tells TanStack Table not to sort `items` itself; the parent
200
+ must re-fetch/re-sort when `onSortingChange` fires
201
+ - `onSortingChange` — receives the new `SortingState`; pass it to your query as
202
+ an `orderBy` parameter
203
+ - `state.sorting` — controlled sort state, keeps headers in sync with data
204
+
205
+ To make a specific column sortable, set `enableSorting: true` on its
206
+ `ColumnDef`. To disable sorting on a specific column, set
207
+ `enableSorting: false`.
208
+
209
+ ## Spinner overlay (external loading, before table mounts)
210
+
211
+ Use `DataTable`'s `isLoading` prop for in-table loading. Only use an external
212
+ Spinner overlay when the table cannot mount yet — e.g. a full-region loading
213
+ state before you have any data at all, or a loading state that covers the entire
214
+ page section including the filter bar:
215
+
216
+ ```tsx
217
+ {
218
+ isLoading ? (
219
+ <Box
220
+ display="flex"
221
+ flexDirection="column"
222
+ alignItems="center"
223
+ justifyContent="center"
224
+ gap="small"
225
+ padding="xlarge"
226
+ >
227
+ <Spinner tone="primary" size="xsmall" />
228
+ <Text tone="muted">Loading</Text>
229
+ </Box>
230
+ ) : (
231
+ <DataTable items={data} columns={columns} isLoading={isFetching} />
232
+ );
233
+ }
234
+ ```
235
+
236
+ The distinction:
237
+
238
+ - `isLoading` (initial, no table yet) → external Spinner overlay
239
+ - `isFetching` (refetch while table exists) → pass to `DataTable` `isLoading`
240
+
241
+ ## Do NOTs
242
+
243
+ - NEVER raw hex values — always use theme color tokens
244
+ - NEVER raw pixel values — always use theme spacing/sizing tokens
245
+ - NEVER Tailwind classes
246
+ - NEVER `ChevronUp/DownIcon` for sort — use `SortAscending/DescendingIcon`
247
+ - NEVER `primaryMuted` for selected rows — use `primarySoft`
248
+ - NEVER `neutralHover` for row hover — use `color.background.inputPressed`
249
+ - NEVER apply row hover to a non-clickable row — only rows with
250
+ `enableClickableRow` get hover
251
+ - NEVER let row hover fire when the cursor is over a button inside the row — use
252
+ `:hover:not(:has(button:hover))` to suppress it
253
+ - NEVER use `onRowClick` for URL-destination navigation — it breaks "Open in new
254
+ tab", cmd/ctrl-click, and middle-click. Wrap cell content in your router's
255
+ `<Link>` instead. See "Row-as-link navigation".
256
+ - NEVER put pagination inside `DataTable` — caller handles it externally
257
+ - NEVER render an external `<Spinner>` alongside a mounted `DataTable` — use the
258
+ `isLoading` prop instead. An external Spinner is only correct before the table
259
+ can mount (initial page load with no data yet).
260
+ - NEVER pass a string class from `@emotion/css` to `className` or
261
+ `headerClassName` — these props expect `SerializedStyles` from
262
+ `@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, };