@terpjs/react-core 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +2 -2
  2. package/src/AppShell.test.tsx +33 -12
  3. package/src/AppShell.tsx +69 -249
  4. package/src/Breadcrumbs.test.tsx +24 -0
  5. package/src/Breadcrumbs.tsx +9 -32
  6. package/src/ConfirmDialog.tsx +13 -44
  7. package/src/EmptyState.tsx +8 -36
  8. package/src/ErrorState.tsx +8 -36
  9. package/src/Field.test.tsx +57 -0
  10. package/src/Field.tsx +46 -22
  11. package/src/HubPage.test.tsx +22 -13
  12. package/src/HubPage.tsx +25 -97
  13. package/src/LoadingState.tsx +3 -24
  14. package/src/PageActions.tsx +5 -10
  15. package/src/UserMenu.test.tsx +12 -5
  16. package/src/UserMenu.tsx +33 -62
  17. package/src/dataview/DataView.test.tsx +109 -5
  18. package/src/dataview/DataView.tsx +41 -23
  19. package/src/dataview/DataViewCardList.tsx +14 -60
  20. package/src/dataview/DataViewColumnSettings.tsx +46 -51
  21. package/src/dataview/DataViewExpandableRow.tsx +2 -17
  22. package/src/dataview/DataViewPagination.tsx +2 -32
  23. package/src/dataview/DataViewRowActions.tsx +13 -33
  24. package/src/dataview/DataViewTable.tsx +16 -103
  25. package/src/dataview/DataViewToolbar.tsx +53 -76
  26. package/src/dataview/README.md +6 -0
  27. package/src/dataview/index.ts +1 -0
  28. package/src/dataview/internal.tsx +4 -1
  29. package/src/dataview/types.ts +13 -0
  30. package/src/feedback.test.tsx +26 -0
  31. package/src/files.test.tsx +18 -0
  32. package/src/files.tsx +13 -4
  33. package/src/icons.test.tsx +10 -6
  34. package/src/icons.tsx +9 -37
  35. package/src/index.ts +0 -3
  36. package/src/layout.test.tsx +24 -9
  37. package/src/layout.tsx +24 -21
  38. package/src/layoutContract.test.tsx +95 -0
  39. package/src/locale.tsx +24 -4
  40. package/src/markers.test.ts +242 -19
  41. package/src/router.tsx +6 -9
  42. package/src/ssr.test.tsx +1 -3
  43. package/src/styles.test.ts +823 -6
  44. package/src/styles.ts +2699 -153
  45. package/src/theme.tsx +24 -3
  46. package/src/toast.tsx +35 -71
  47. package/src/ui/Alert.test.tsx +12 -0
  48. package/src/ui/Alert.tsx +15 -43
  49. package/src/ui/Badge.test.tsx +14 -3
  50. package/src/ui/Badge.tsx +9 -28
  51. package/src/ui/Button.test.tsx +17 -4
  52. package/src/ui/Button.tsx +10 -63
  53. package/src/ui/Card.test.tsx +6 -2
  54. package/src/ui/Card.tsx +11 -39
  55. package/src/ui/Checkbox.tsx +2 -19
  56. package/src/ui/Combobox.test.tsx +22 -0
  57. package/src/ui/Combobox.tsx +31 -80
  58. package/src/ui/DatePicker.test.tsx +131 -4
  59. package/src/ui/DatePicker.tsx +158 -106
  60. package/src/ui/Input.tsx +6 -19
  61. package/src/ui/Markdown.test.tsx +26 -0
  62. package/src/ui/Markdown.tsx +28 -2
  63. package/src/ui/Menu.test.tsx +38 -4
  64. package/src/ui/Menu.tsx +50 -52
  65. package/src/ui/Popover.tsx +53 -19
  66. package/src/ui/Radio.tsx +5 -30
  67. package/src/ui/Select.tsx +7 -30
  68. package/src/ui/Switch.tsx +2 -20
  69. package/src/ui/Tabs.tsx +4 -28
  70. package/src/ui/Textarea.tsx +6 -17
  71. package/src/ui/Tooltip.tsx +9 -21
  72. package/src/ui/controlStyles.ts +0 -9
@@ -1,5 +1,8 @@
1
+ import { useId } from "react";
2
+
3
+ import { Popover } from "../ui/Popover";
1
4
  import { ArrowDownGlyph, ArrowUpGlyph, ColumnsGlyph } from "./glyphs";
2
- import { DataViewMenu, useDataViewText } from "./internal";
5
+ import { useDataViewText } from "./internal";
3
6
  import type { DataViewColumn } from "./types";
4
7
 
5
8
  export interface DataViewColumnSettingsProps<T> {
@@ -8,68 +11,72 @@ export interface DataViewColumnSettingsProps<T> {
8
11
  columnVisibility: Record<string, boolean>;
9
12
  onColumnVisibleChange: (columnId: string, visible: boolean) => void;
10
13
  onMoveColumn: (columnId: string, direction: -1 | 1) => void;
14
+ /**
15
+ * Render the panel open on mount — the same escape the menu primitive already exposes,
16
+ * and the only way a portalled panel reaches a per-specimen visual lane.
17
+ */
18
+ defaultOpen?: boolean;
11
19
  }
12
20
 
13
21
  /**
14
- * The "view options" menu: per-column show/hide checkboxes and up/down reordering.
22
+ * The "view options" panel: per-column show/hide checkboxes and up/down reordering.
15
23
  * Only user columns appear — the pinned system columns (select/expand/actions) are
16
24
  * never hideable or reorderable, and are skipped when computing reorder targets
17
25
  * because this list simply does not contain them.
26
+ *
27
+ * A `Popover` rather than a `Menu`, and the distinction is a correctness one rather than
28
+ * a preference. `Menu` stamps `role="menu"`, which ARIA requires to own only
29
+ * `menuitem` / `menuitemradio` / `menuitemcheckbox` / `group` children — and this panel's
30
+ * content is a heading, labelled checkboxes and paired reorder buttons, which is a small
31
+ * FORM. Feeding it to a menu produced a critical `aria-required-children` violation in
32
+ * every theme, and there is no arrangement that fixes it while keeping the interaction:
33
+ * reorder buttons inside a `menuitemcheckbox` would be interactive descendants of a menu
34
+ * item, which ARIA forbids in turn. The panel is therefore a group labelled by its own
35
+ * heading, navigated with Tab the way a form is, and `Popover` supplies the Escape,
36
+ * outside-click and focus-return behaviour `Menu` was being used for.
37
+ *
38
+ * Nothing had rendered this panel open before, which is why a shipped critical defect was
39
+ * invisible: the screenshot lane clips to the specimen and the panel portals to the body,
40
+ * and no specimen opened it at all.
18
41
  */
19
42
  export function DataViewColumnSettings<T>({
20
43
  columns,
21
44
  columnVisibility,
22
45
  onColumnVisibleChange,
23
46
  onMoveColumn,
47
+ defaultOpen,
24
48
  }: DataViewColumnSettingsProps<T>) {
25
49
  const { strings, resolve } = useDataViewText();
50
+ const headingId = useId();
26
51
 
27
52
  return (
28
- <DataViewMenu
53
+ <Popover
54
+ defaultOpen={defaultOpen}
55
+ focusOnOpen
56
+ data-owner="dataview-column-settings"
29
57
  trigger={
30
- <>
58
+ // The trigger keeps the menu-trigger marker, which is styling vocabulary rather than
59
+ // a semantic claim — the sheet describes it as the package's outlined control, and
60
+ // reusing it is what keeps this control's appearance identical. What it does NOT keep
61
+ // is aria-haspopup="menu", because the panel is no longer a menu; Popover supplies
62
+ // aria-expanded and aria-controls, which is the whole contract for a disclosure.
63
+ <button type="button" data-terp="menu-trigger">
31
64
  <ColumnsGlyph />
32
- <span style={{ fontSize: "var(--font-size-sm)" }}>{resolve(strings.viewOptions)}</span>
33
- </>
65
+ <span>{resolve(strings.viewOptions)}</span>
66
+ </button>
34
67
  }
35
- triggerLabel={resolve(strings.viewOptions)}
36
68
  >
37
69
  {() => (
38
- <div style={{ display: "grid", gap: "var(--space-1)" }}>
39
- <div
40
- style={{
41
- padding: "var(--space-1) var(--space-2)",
42
- fontSize: "var(--font-size-sm)",
43
- fontWeight: "var(--font-weight-medium)" as never,
44
- color: "var(--color-neutral-500)",
45
- }}
46
- >
70
+ <div data-terp="dataview-column-settings" role="group" aria-labelledby={headingId}>
71
+ <div id={headingId} data-terp="dataview-column-settings-title">
47
72
  {resolve(strings.columns)}
48
73
  </div>
49
74
  {columns.map((column, index) => {
50
75
  const label = resolve(column.meta?.label ?? column.header);
51
76
  const visible = columnVisibility[column.id] !== false;
52
77
  return (
53
- <div
54
- key={column.id}
55
- style={{
56
- display: "flex",
57
- alignItems: "center",
58
- gap: "var(--space-2)",
59
- padding: "var(--space-1) var(--space-2)",
60
- borderRadius: "var(--radius-sm)",
61
- }}
62
- >
63
- <label
64
- style={{
65
- display: "flex",
66
- alignItems: "center",
67
- gap: "var(--space-2)",
68
- flex: 1,
69
- cursor: "pointer",
70
- fontSize: "var(--font-size-sm)",
71
- }}
72
- >
78
+ <div key={column.id} data-terp="dataview-column-option">
79
+ <label>
73
80
  <input
74
81
  type="checkbox"
75
82
  checked={visible}
@@ -80,19 +87,19 @@ export function DataViewColumnSettings<T>({
80
87
  </label>
81
88
  <button
82
89
  type="button"
90
+ data-terp="iconbutton"
83
91
  aria-label={`${resolve(strings.moveUp)}: ${label}`}
84
92
  disabled={index === 0}
85
93
  onClick={() => onMoveColumn(column.id, -1)}
86
- style={reorderButtonStyle(index === 0)}
87
94
  >
88
95
  <ArrowUpGlyph size={14} />
89
96
  </button>
90
97
  <button
91
98
  type="button"
99
+ data-terp="iconbutton"
92
100
  aria-label={`${resolve(strings.moveDown)}: ${label}`}
93
101
  disabled={index === columns.length - 1}
94
102
  onClick={() => onMoveColumn(column.id, 1)}
95
- style={reorderButtonStyle(index === columns.length - 1)}
96
103
  >
97
104
  <ArrowDownGlyph size={14} />
98
105
  </button>
@@ -101,18 +108,6 @@ export function DataViewColumnSettings<T>({
101
108
  })}
102
109
  </div>
103
110
  )}
104
- </DataViewMenu>
111
+ </Popover>
105
112
  );
106
113
  }
107
-
108
- function reorderButtonStyle(disabled: boolean) {
109
- return {
110
- display: "inline-flex",
111
- padding: "var(--space-1)",
112
- background: "transparent",
113
- border: "none",
114
- borderRadius: "var(--radius-sm)",
115
- cursor: disabled ? "not-allowed" : "pointer",
116
- color: disabled ? "var(--color-neutral-300)" : "var(--color-neutral-700)",
117
- } as const;
118
- }
@@ -20,19 +20,11 @@ export function DataViewExpandToggle({
20
20
  type="button"
21
21
  aria-label={resolve(expanded ? strings.collapseRow : strings.expandRow)}
22
22
  aria-expanded={expanded}
23
+ data-terp="iconbutton"
23
24
  onClick={(event) => {
24
25
  event.stopPropagation();
25
26
  onToggle();
26
27
  }}
27
- style={{
28
- display: "inline-flex",
29
- padding: "var(--space-1)",
30
- background: "transparent",
31
- border: "none",
32
- borderRadius: "var(--radius-sm)",
33
- cursor: "pointer",
34
- color: "var(--color-neutral-500)",
35
- }}
36
28
  >
37
29
  {expanded ? <ChevronDownGlyph /> : <ChevronRightGlyph />}
38
30
  </button>
@@ -52,14 +44,7 @@ export function DataViewExpandableRow({
52
44
  }) {
53
45
  return (
54
46
  <tr>
55
- <td
56
- colSpan={colSpan}
57
- style={{
58
- padding: "var(--space-3) var(--space-4)",
59
- background: "var(--color-neutral-50)",
60
- borderBottom: "1px solid var(--color-neutral-200)",
61
- }}
62
- >
47
+ <td colSpan={colSpan} data-terp="dataview-expanded-cell">
63
48
  {children}
64
49
  </td>
65
50
  </tr>
@@ -1,5 +1,3 @@
1
- import type { CSSProperties } from "react";
2
-
3
1
  import { useDataViewText } from "./internal";
4
2
  import {
5
3
  PageFirstGlyph,
@@ -15,18 +13,6 @@ export interface DataViewPaginationProps {
15
13
  onPaginationChange: (pagination: DataViewPaginationState) => void;
16
14
  }
17
15
 
18
- const pagerButtonStyle = (disabled: boolean): CSSProperties => ({
19
- display: "inline-flex",
20
- alignItems: "center",
21
- minHeight: "2rem",
22
- padding: "var(--space-1) var(--space-2)",
23
- background: "var(--color-neutral-0)",
24
- border: "1px solid var(--color-neutral-300)",
25
- borderRadius: "var(--radius-md)",
26
- cursor: disabled ? "not-allowed" : "pointer",
27
- color: disabled ? "var(--color-neutral-300)" : "var(--color-neutral-700)",
28
- });
29
-
30
16
  /**
31
17
  * The footer pagination bar: "X–Y of Z results", the current page / page count, and
32
18
  * first / prev / next / last controls (disabled at bounds; page controls hidden when
@@ -49,22 +35,10 @@ export function DataViewPagination({
49
35
  const atLast = pageIndex >= pageCount - 1;
50
36
 
51
37
  return (
52
- <div
53
- style={{
54
- display: "flex",
55
- alignItems: "center",
56
- justifyContent: "space-between",
57
- gap: "var(--space-3)",
58
- flexWrap: "wrap",
59
- padding: "var(--space-2) var(--space-3)",
60
- borderTop: "1px solid var(--color-neutral-200)",
61
- fontSize: "var(--font-size-sm)",
62
- color: "var(--color-neutral-500)",
63
- }}
64
- >
38
+ <div data-terp="dataview-pagination">
65
39
  <span>{format(strings.resultsRange, { from, to, total: totalCount })}</span>
66
40
  {pageCount > 1 && (
67
- <span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-2)" }}>
41
+ <span data-terp="dataview-pager">
68
42
  <span>{format(strings.pageOf, { page: pageIndex + 1, pages: pageCount })}</span>
69
43
  <button
70
44
  type="button"
@@ -72,7 +46,6 @@ export function DataViewPagination({
72
46
  disabled={atFirst}
73
47
  onClick={() => goTo(0)}
74
48
  data-terp="iconbutton"
75
- style={pagerButtonStyle(atFirst)}
76
49
  >
77
50
  <PageFirstGlyph />
78
51
  </button>
@@ -82,7 +55,6 @@ export function DataViewPagination({
82
55
  disabled={atFirst}
83
56
  onClick={() => goTo(pageIndex - 1)}
84
57
  data-terp="iconbutton"
85
- style={pagerButtonStyle(atFirst)}
86
58
  >
87
59
  <PagePrevGlyph />
88
60
  </button>
@@ -92,7 +64,6 @@ export function DataViewPagination({
92
64
  disabled={atLast}
93
65
  onClick={() => goTo(pageIndex + 1)}
94
66
  data-terp="iconbutton"
95
- style={pagerButtonStyle(atLast)}
96
67
  >
97
68
  <PageNextGlyph />
98
69
  </button>
@@ -102,7 +73,6 @@ export function DataViewPagination({
102
73
  disabled={atLast}
103
74
  onClick={() => goTo(pageCount - 1)}
104
75
  data-terp="iconbutton"
105
- style={pagerButtonStyle(atLast)}
106
76
  >
107
77
  <PageLastGlyph />
108
78
  </button>
@@ -14,6 +14,12 @@ export interface DataViewRowActionsProps<T> {
14
14
  layout: DataViewRowActionsLayout;
15
15
  /** On mobile all standard actions collapse into the menu regardless of flags. */
16
16
  isMobile: boolean;
17
+ /**
18
+ * Render the overflow menu open on mount — the same escape `Menu`, `Popover` and both
19
+ * date pickers already expose, and for the same reason: a portalled panel is invisible
20
+ * to a per-specimen visual lane unless something can name its open state.
21
+ */
22
+ defaultOpen?: boolean;
17
23
  }
18
24
 
19
25
  function InlineActionButton<T>({ action, row }: { action: DataViewRowAction<T>; row: T }) {
@@ -23,30 +29,12 @@ function InlineActionButton<T>({ action, row }: { action: DataViewRowAction<T>;
23
29
  return (
24
30
  <button
25
31
  type="button"
32
+ data-terp="dataview-inline-action"
33
+ data-destructive={destructive || undefined}
26
34
  disabled={disabled}
27
35
  onClick={() => action.onClick?.(row)}
28
- style={{
29
- font: "inherit",
30
- fontSize: "var(--font-size-sm)",
31
- display: "inline-flex",
32
- alignItems: "center",
33
- minHeight: "2rem",
34
- gap: "var(--space-1)",
35
- padding: "var(--space-1) var(--space-2)",
36
- background: "transparent",
37
- border: "1px solid var(--color-neutral-300)",
38
- borderRadius: "var(--radius-md)",
39
- cursor: disabled ? "not-allowed" : "pointer",
40
- color: disabled
41
- ? "var(--color-neutral-300)"
42
- : destructive
43
- ? "var(--color-status-danger)"
44
- : "var(--color-neutral-700)",
45
- }}
46
36
  >
47
- {action.icon !== undefined && (
48
- <span aria-hidden style={{ display: "inline-flex" }}>{action.icon}</span>
49
- )}
37
+ {action.icon !== undefined && <span aria-hidden>{action.icon}</span>}
50
38
  {resolve(action.label)}
51
39
  </button>
52
40
  );
@@ -62,6 +50,7 @@ export function DataViewRowActions<T>({
62
50
  actions,
63
51
  layout,
64
52
  isMobile,
53
+ defaultOpen,
65
54
  }: DataViewRowActionsProps<T>) {
66
55
  const { strings, resolve } = useDataViewText();
67
56
 
@@ -83,21 +72,11 @@ export function DataViewRowActions<T>({
83
72
  const stop = (event: MouseEvent) => event.stopPropagation();
84
73
 
85
74
  const renderCustom = (action: DataViewRowAction<T>, index: number): ReactNode => (
86
- <span key={`custom-${index}`} style={{ display: "inline-flex" }}>
87
- {action.render?.(row)}
88
- </span>
75
+ <span key={`custom-${index}`}>{action.render?.(row)}</span>
89
76
  );
90
77
 
91
78
  return (
92
- <span
93
- onClick={stop}
94
- style={{
95
- display: "inline-flex",
96
- alignItems: "center",
97
- justifyContent: "flex-end",
98
- gap: "var(--space-1)",
99
- }}
100
- >
79
+ <span data-terp="dataview-row-actions" onClick={stop}>
101
80
  {custom.map(renderCustom)}
102
81
  {inline.map((action, index) => (
103
82
  <InlineActionButton key={`inline-${index}`} action={action} row={row} />
@@ -106,6 +85,7 @@ export function DataViewRowActions<T>({
106
85
  <DataViewMenu
107
86
  trigger={<EllipsisGlyph />}
108
87
  triggerLabel={resolve(strings.moreActions)}
88
+ defaultOpen={defaultOpen}
109
89
  >
110
90
  {(close) => (
111
91
  <>
@@ -1,9 +1,8 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import type { CSSProperties, ReactNode } from "react";
2
+ import type { ReactNode } from "react";
3
3
 
4
4
  import { injectTerpStyles } from "../styles";
5
5
  import type { BadgeTone } from "../ui/Badge";
6
- import { toneSoftColors } from "../ui/Badge";
7
6
  import type { UiText } from "../uiText";
8
7
  import { DataViewExpandToggle, DataViewExpandableRow } from "./DataViewExpandableRow";
9
8
  import { DataViewRowActions } from "./DataViewRowActions";
@@ -46,41 +45,6 @@ export interface DataViewTableProps<T> {
46
45
  rowActionsLayout: DataViewRowActionsLayout;
47
46
  }
48
47
 
49
- const headerCellStyle: CSSProperties = {
50
- position: "relative",
51
- padding: "var(--space-2) var(--space-3)",
52
- textAlign: "left",
53
- fontSize: "var(--font-size-xs)",
54
- fontWeight: "var(--font-weight-semibold)" as never,
55
- color: "var(--color-neutral-500)",
56
- textTransform: "uppercase",
57
- letterSpacing: "0.04em",
58
- borderBottom: "1px solid var(--color-neutral-200)",
59
- whiteSpace: "nowrap",
60
- background: "var(--color-neutral-0)",
61
- };
62
-
63
- const bodyCellStyle: CSSProperties = {
64
- padding: "var(--space-3)",
65
- borderBottom: "1px solid var(--color-neutral-100)",
66
- fontSize: "var(--font-size-sm)",
67
- color: "var(--color-neutral-900)",
68
- overflow: "hidden",
69
- textOverflow: "ellipsis",
70
- };
71
-
72
- const recordButtonStyle: CSSProperties = {
73
- position: "absolute",
74
- width: 1,
75
- height: 1,
76
- padding: 0,
77
- margin: -1,
78
- overflow: "hidden",
79
- clip: "rect(0 0 0 0)",
80
- whiteSpace: "nowrap",
81
- border: 0,
82
- };
83
-
84
48
  /**
85
49
  * The table layout of {@link DataView}: sortable, resizable headers; system columns
86
50
  * (expand → select → user columns → actions); row click, selection, expansion.
@@ -166,17 +130,13 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
166
130
  <table
167
131
  ref={tableRef}
168
132
  data-terp="dataview-table"
169
- style={{
170
- width: "100%",
171
- borderCollapse: "collapse",
172
- tableLayout: liveSizing !== null ? "fixed" : "auto",
173
- }}
133
+ data-resizing={liveSizing !== null || undefined}
174
134
  >
175
135
  <thead>
176
136
  <tr>
177
- {hasExpand && <th style={{ ...headerCellStyle, width: 40 }} aria-hidden />}
137
+ {hasExpand && <th data-terp="dataview-expand-cell" aria-hidden />}
178
138
  {props.selectionEnabled && (
179
- <th style={{ ...headerCellStyle, width: 40 }}>
139
+ <th data-terp="dataview-select-cell">
180
140
  <input
181
141
  ref={selectAllRef}
182
142
  type="checkbox"
@@ -197,27 +157,17 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
197
157
  aria-sort={
198
158
  sort === undefined ? undefined : sort.desc ? "descending" : "ascending"
199
159
  }
200
- style={{ ...headerCellStyle, width }}
160
+ style={width === undefined ? undefined : { width }}
201
161
  >
202
162
  {sortable ? (
203
163
  <button
204
164
  type="button"
165
+ data-terp="dataview-column-sort"
205
166
  onClick={() => props.onToggleSort(column.id)}
206
- style={{
207
- font: "inherit",
208
- color: "inherit",
209
- display: "inline-flex",
210
- alignItems: "center",
211
- gap: "var(--space-1)",
212
- background: "transparent",
213
- border: "none",
214
- padding: 0,
215
- cursor: "pointer",
216
- }}
217
167
  >
218
168
  {resolve(column.header)}
219
169
  {sort === undefined ? (
220
- <SortNoneGlyph size={12} style={{ opacity: 0.5 }} />
170
+ <SortNoneGlyph size={12} />
221
171
  ) : sort.desc ? (
222
172
  <SortDescGlyph size={12} />
223
173
  ) : (
@@ -231,44 +181,18 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
231
181
  role="separator"
232
182
  aria-orientation="vertical"
233
183
  aria-label={`${resolve(strings.resizeColumn)}: ${resolve(column.meta?.label ?? column.header)}`}
184
+ data-terp="dataview-column-resizer"
234
185
  onPointerDown={(event) => {
235
186
  event.preventDefault();
236
187
  startResize(column.id, event.clientX);
237
188
  }}
238
- style={{
239
- position: "absolute",
240
- top: 0,
241
- // Sit flush with the cell's right edge — a negative offset would
242
- // let the last column's handle spill past the table and trip the
243
- // scroll container's overflow-x, adding a spurious scrollbar.
244
- right: 0,
245
- width: 7,
246
- height: "100%",
247
- cursor: "col-resize",
248
- zIndex: 1,
249
- touchAction: "none",
250
- }}
251
189
  />
252
190
  </th>
253
191
  );
254
192
  })}
255
193
  {hasActions && (
256
- <th style={{ ...headerCellStyle, width: 56 }}>
257
- <span
258
- style={{
259
- position: "absolute",
260
- width: 1,
261
- height: 1,
262
- padding: 0,
263
- margin: -1,
264
- overflow: "hidden",
265
- clip: "rect(0 0 0 0)",
266
- whiteSpace: "nowrap",
267
- border: 0,
268
- }}
269
- >
270
- {resolve(strings.actions)}
271
- </span>
194
+ <th data-terp="dataview-actions-cell">
195
+ <span>{resolve(strings.actions)}</span>
272
196
  </th>
273
197
  )}
274
198
  </tr>
@@ -283,23 +207,13 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
283
207
  <RowGroup key={rowId}>
284
208
  <tr
285
209
  onClick={clickable ? () => props.onRowClick?.(row) : undefined}
286
- data-terp={clickable ? "dataview-row" : undefined}
210
+ data-terp="dataview-row"
211
+ data-clickable={clickable || undefined}
287
212
  data-selected={props.isSelected(rowId) || undefined}
288
213
  data-tone={tone ?? undefined}
289
- style={{
290
- cursor: clickable ? "pointer" : undefined,
291
- // A row's own state outranks the selection tint — selection still
292
- // shows via the checkbox and data-selected.
293
- background:
294
- tone !== null
295
- ? toneSoftColors[tone]
296
- : props.isSelected(rowId)
297
- ? "var(--color-neutral-50)"
298
- : undefined,
299
- }}
300
214
  >
301
215
  {hasExpand && (
302
- <td style={bodyCellStyle}>
216
+ <td data-terp="dataview-expand-cell">
303
217
  <DataViewExpandToggle
304
218
  expanded={expanded}
305
219
  onToggle={() => props.onToggleExpanded(rowId)}
@@ -307,7 +221,7 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
307
221
  </td>
308
222
  )}
309
223
  {props.selectionEnabled && (
310
- <td style={bodyCellStyle} onClick={(event) => event.stopPropagation()}>
224
+ <td data-terp="dataview-select-cell" onClick={(event) => event.stopPropagation()}>
311
225
  <input
312
226
  type="checkbox"
313
227
  aria-label={resolve(strings.selectRow)}
@@ -317,7 +231,7 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
317
231
  </td>
318
232
  )}
319
233
  {props.columns.map((column) => (
320
- <td key={column.id} style={bodyCellStyle}>
234
+ <td key={column.id}>
321
235
  {clickable && column === props.columns[0] && (
322
236
  <button
323
237
  type="button"
@@ -329,7 +243,6 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
329
243
  event.stopPropagation();
330
244
  props.onRowClick?.(row);
331
245
  }}
332
- style={recordButtonStyle}
333
246
  />
334
247
  )}
335
248
  {column.cell !== undefined
@@ -338,7 +251,7 @@ export function DataViewTable<T>(props: DataViewTableProps<T>) {
338
251
  </td>
339
252
  ))}
340
253
  {hasActions && (
341
- <td style={{ ...bodyCellStyle, textAlign: "right" }}>
254
+ <td data-terp="dataview-actions-cell">
342
255
  <DataViewRowActions
343
256
  row={row}
344
257
  actions={props.rowActions?.(row) ?? []}