@apify/ui-library 1.132.1 → 1.133.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 (104) hide show
  1. package/dist/src/components/index.d.ts +1 -0
  2. package/dist/src/components/index.d.ts.map +1 -1
  3. package/dist/src/components/index.js +1 -0
  4. package/dist/src/components/index.js.map +1 -1
  5. package/dist/src/components/link.d.ts +3 -0
  6. package/dist/src/components/link.d.ts.map +1 -1
  7. package/dist/src/components/link.js +2 -2
  8. package/dist/src/components/link.js.map +1 -1
  9. package/dist/src/components/table/index.d.ts +17 -0
  10. package/dist/src/components/table/index.d.ts.map +1 -0
  11. package/dist/src/components/table/index.js +16 -0
  12. package/dist/src/components/table/index.js.map +1 -0
  13. package/dist/src/components/table/table.context.d.ts +3 -0
  14. package/dist/src/components/table/table.context.d.ts.map +1 -0
  15. package/dist/src/components/table/table.context.js +3 -0
  16. package/dist/src/components/table/table.context.js.map +1 -0
  17. package/dist/src/components/table/table.d.ts +3 -0
  18. package/dist/src/components/table/table.d.ts.map +1 -0
  19. package/dist/src/components/table/table.js +6 -0
  20. package/dist/src/components/table/table.js.map +1 -0
  21. package/dist/src/components/table/table.styled.d.ts +30 -0
  22. package/dist/src/components/table/table.styled.d.ts.map +1 -0
  23. package/dist/src/components/table/table.styled.js +194 -0
  24. package/dist/src/components/table/table.styled.js.map +1 -0
  25. package/dist/src/components/table/table_body.d.ts +3 -0
  26. package/dist/src/components/table/table_body.d.ts.map +1 -0
  27. package/dist/src/components/table/table_body.js +6 -0
  28. package/dist/src/components/table/table_body.js.map +1 -0
  29. package/dist/src/components/table/table_cell.d.ts +10 -0
  30. package/dist/src/components/table/table_cell.d.ts.map +1 -0
  31. package/dist/src/components/table/table_cell.js +18 -0
  32. package/dist/src/components/table/table_cell.js.map +1 -0
  33. package/dist/src/components/table/table_empty_row.d.ts +6 -0
  34. package/dist/src/components/table/table_empty_row.d.ts.map +1 -0
  35. package/dist/src/components/table/table_empty_row.js +5 -0
  36. package/dist/src/components/table/table_empty_row.js.map +1 -0
  37. package/dist/src/components/table/table_error_row.d.ts +7 -0
  38. package/dist/src/components/table/table_error_row.d.ts.map +1 -0
  39. package/dist/src/components/table/table_error_row.js +6 -0
  40. package/dist/src/components/table/table_error_row.js.map +1 -0
  41. package/dist/src/components/table/table_expansion_row.d.ts +10 -0
  42. package/dist/src/components/table/table_expansion_row.d.ts.map +1 -0
  43. package/dist/src/components/table/table_expansion_row.js +10 -0
  44. package/dist/src/components/table/table_expansion_row.js.map +1 -0
  45. package/dist/src/components/table/table_foot.d.ts +3 -0
  46. package/dist/src/components/table/table_foot.d.ts.map +1 -0
  47. package/dist/src/components/table/table_foot.js +6 -0
  48. package/dist/src/components/table/table_foot.js.map +1 -0
  49. package/dist/src/components/table/table_head.d.ts +3 -0
  50. package/dist/src/components/table/table_head.d.ts.map +1 -0
  51. package/dist/src/components/table/table_head.js +6 -0
  52. package/dist/src/components/table/table_head.js.map +1 -0
  53. package/dist/src/components/table/table_head_cell.d.ts +3 -0
  54. package/dist/src/components/table/table_head_cell.d.ts.map +1 -0
  55. package/dist/src/components/table/table_head_cell.js +6 -0
  56. package/dist/src/components/table/table_head_cell.js.map +1 -0
  57. package/dist/src/components/table/table_head_row.d.ts +3 -0
  58. package/dist/src/components/table/table_head_row.d.ts.map +1 -0
  59. package/dist/src/components/table/table_head_row.js +6 -0
  60. package/dist/src/components/table/table_head_row.js.map +1 -0
  61. package/dist/src/components/table/table_loading_row.d.ts +6 -0
  62. package/dist/src/components/table/table_loading_row.d.ts.map +1 -0
  63. package/dist/src/components/table/table_loading_row.js +6 -0
  64. package/dist/src/components/table/table_loading_row.js.map +1 -0
  65. package/dist/src/components/table/table_row.d.ts +4 -0
  66. package/dist/src/components/table/table_row.d.ts.map +1 -0
  67. package/dist/src/components/table/table_row.js +12 -0
  68. package/dist/src/components/table/table_row.js.map +1 -0
  69. package/dist/src/components/table/table_test_ids.d.ts +16 -0
  70. package/dist/src/components/table/table_test_ids.d.ts.map +1 -0
  71. package/dist/src/components/table/table_test_ids.js +16 -0
  72. package/dist/src/components/table/table_test_ids.js.map +1 -0
  73. package/dist/src/components/table/table_wrapper.d.ts +4 -0
  74. package/dist/src/components/table/table_wrapper.d.ts.map +1 -0
  75. package/dist/src/components/table/table_wrapper.js +49 -0
  76. package/dist/src/components/table/table_wrapper.js.map +1 -0
  77. package/dist/src/components/table/types.d.ts +21 -0
  78. package/dist/src/components/table/types.d.ts.map +1 -0
  79. package/dist/src/components/table/types.js +2 -0
  80. package/dist/src/components/table/types.js.map +1 -0
  81. package/dist/tsconfig.build.tsbuildinfo +1 -1
  82. package/package.json +3 -3
  83. package/src/components/index.ts +1 -0
  84. package/src/components/link.stories.tsx +12 -0
  85. package/src/components/link.tsx +9 -0
  86. package/src/components/table/index.ts +16 -0
  87. package/src/components/table/table.context.ts +5 -0
  88. package/src/components/table/table.stories.tsx +258 -0
  89. package/src/components/table/table.styled.ts +207 -0
  90. package/src/components/table/table.tsx +9 -0
  91. package/src/components/table/table_body.tsx +9 -0
  92. package/src/components/table/table_cell.tsx +28 -0
  93. package/src/components/table/table_empty_row.tsx +12 -0
  94. package/src/components/table/table_error_row.tsx +13 -0
  95. package/src/components/table/table_expansion_row.tsx +24 -0
  96. package/src/components/table/table_foot.tsx +9 -0
  97. package/src/components/table/table_head.tsx +9 -0
  98. package/src/components/table/table_head_cell.tsx +9 -0
  99. package/src/components/table/table_head_row.tsx +9 -0
  100. package/src/components/table/table_loading_row.tsx +13 -0
  101. package/src/components/table/table_row.tsx +30 -0
  102. package/src/components/table/table_test_ids.ts +15 -0
  103. package/src/components/table/table_wrapper.tsx +71 -0
  104. package/src/components/table/types.ts +24 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apify/ui-library",
3
- "version": "1.132.1",
3
+ "version": "1.133.0",
4
4
  "description": "React UI library used by apify.com",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  "postpublish": "npm run clean"
24
24
  },
25
25
  "//": [
26
- "Storybook for the components lives in a separate package components-storybook.",
26
+ "Storybook for the components lives in a separate package ui-storybook.",
27
27
  "It's not nice, but helps us to get around the problem of multiple react instances."
28
28
  ],
29
29
  "dependencies": {
@@ -70,5 +70,5 @@
70
70
  "src",
71
71
  "style"
72
72
  ],
73
- "gitHead": "10415388fd2ce4e24af29ddfe3090214c4a48b51"
73
+ "gitHead": "4df09cdffad169b379e062cc4dbc1641d8a6f0c7"
74
74
  }
@@ -30,3 +30,4 @@ export * from './checkbox/index.js';
30
30
  export * from './collapsible_card/index.js';
31
31
  export * from './select/index.js';
32
32
  export * from './switch/index.js';
33
+ export * from './table/index.js';
@@ -69,6 +69,18 @@ export default {
69
69
  control: 'text',
70
70
  description: 'The link text or content to display',
71
71
  },
72
+ ariaHidden: {
73
+ control: 'boolean',
74
+ description: 'Whether the link should be hidden from screen readers (useful for decorative links)',
75
+ },
76
+ ariaLabel: {
77
+ control: 'text',
78
+ description: 'Accessible label for screen readers when the link text is not descriptive',
79
+ },
80
+ tabIndex: {
81
+ control: 'number',
82
+ description: 'Tab index for keyboard navigation (default is 0)',
83
+ },
72
84
  },
73
85
  parameters: {
74
86
  design: {
@@ -19,6 +19,9 @@ export interface RegularLinkProps {
19
19
  target?: string,
20
20
  trackingId?: string,
21
21
  trackingData?: object,
22
+ tabIndex?: number,
23
+ ariaHidden?: boolean,
24
+ ariaLabel?: string,
22
25
  }
23
26
 
24
27
  /**
@@ -104,6 +107,9 @@ export const Link = forwardRef<HTMLElement, LinkProps>(({
104
107
  onClick,
105
108
  trackingId,
106
109
  trackingData,
110
+ tabIndex,
111
+ ariaHidden,
112
+ ariaLabel,
107
113
  ...rest
108
114
  }, ref) => {
109
115
  const {
@@ -139,6 +145,9 @@ export const Link = forwardRef<HTMLElement, LinkProps>(({
139
145
  target={target || (isExternal ? '_blank' : '_self')}
140
146
  onClick={trackedOnClick}
141
147
  ref={ref}
148
+ tabIndex={tabIndex}
149
+ aria-hidden={ariaHidden}
150
+ aria-label={ariaLabel}
142
151
  {...rest}
143
152
  >
144
153
  {children}
@@ -0,0 +1,16 @@
1
+ export { Table } from './table.js';
2
+ export { TableHead } from './table_head.js';
3
+ export { TableBody } from './table_body.js';
4
+ export { TableFoot } from './table_foot.js';
5
+ export { TableHeadRow } from './table_head_row.js';
6
+ export { TableHeadCell } from './table_head_cell.js';
7
+ export { TableRow } from './table_row.js';
8
+ export { TableCell, TableCellLink } from './table_cell.js';
9
+ export { HorizontallyScrollableTableWrapper } from './table_wrapper.js';
10
+ export { TableExpansionRow } from './table_expansion_row.js';
11
+ export { TableEmptyRow } from './table_empty_row.js';
12
+ export { TableLoadingRow } from './table_loading_row.js';
13
+ export { TableErrorRow } from './table_error_row.js';
14
+ export { tableClassNames } from './table.styled.js';
15
+ export { tableTestIds } from './table_test_ids.js';
16
+ export type { HorizontallyScrollableTableWrapperProps, TableExpansion, TableRowProps } from './types.js';
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react';
2
+
3
+ import type { To } from '../link.js';
4
+
5
+ export const TableRowLinkContext = createContext<To | undefined>(undefined);
@@ -0,0 +1,258 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { expect, within } from 'storybook/test';
3
+
4
+ import { Text } from '../text/index.js';
5
+ import { Table } from './table.js';
6
+ import { TableBody } from './table_body.js';
7
+ import { TableCell, TableCellLink } from './table_cell.js';
8
+ import { TableEmptyRow } from './table_empty_row.js';
9
+ import { TableErrorRow } from './table_error_row.js';
10
+ import { TableHead } from './table_head.js';
11
+ import { TableHeadCell } from './table_head_cell.js';
12
+ import { TableHeadRow } from './table_head_row.js';
13
+ import { TableLoadingRow } from './table_loading_row.js';
14
+ import { TableRow } from './table_row.js';
15
+ import { HorizontallyScrollableTableWrapper } from './table_wrapper.js';
16
+ import { tableTestIds } from './table_test_ids.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Mock data
20
+ // ---------------------------------------------------------------------------
21
+
22
+ type MockItem = {
23
+ _id: string;
24
+ name: string;
25
+ status: string;
26
+ email: string;
27
+ };
28
+
29
+ const MOCK_DATA: MockItem[] = [
30
+ { _id: '1', name: 'Web Scraper', status: 'active', email: 'alice@example.com' },
31
+ { _id: '2', name: 'Data Extractor', status: 'inactive', email: 'bob@example.com' },
32
+ { _id: '3', name: 'Email Sender', status: 'pending', email: 'carol@example.com' },
33
+ { _id: '4', name: 'PDF Parser', status: 'active', email: 'dave@example.com' },
34
+ ];
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Storybook meta
38
+ // ---------------------------------------------------------------------------
39
+
40
+ const meta: Meta = {
41
+ title: 'UI-Library/Table',
42
+ tags: ['new'],
43
+ component: Table,
44
+ subcomponents: {
45
+ HorizontallyScrollableTableWrapper,
46
+ TableHead,
47
+ TableHeadRow,
48
+ TableHeadCell,
49
+ TableBody,
50
+ TableRow,
51
+ TableCell,
52
+ TableCellLink,
53
+ TableEmptyRow,
54
+ TableLoadingRow,
55
+ TableErrorRow,
56
+ },
57
+ parameters: {
58
+ docs: {
59
+ description: {
60
+ component: 'Compositional table primitives. Compose together to build any table layout. See subcomponents below.',
61
+ },
62
+ },
63
+ },
64
+ };
65
+
66
+ export default meta;
67
+
68
+ type Story = StoryObj;
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Stories
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /** Basic table with primitives only. */
75
+ export const Default: Story = {
76
+ render: () => (
77
+ <HorizontallyScrollableTableWrapper>
78
+ <Table>
79
+ <TableHead>
80
+ <TableHeadRow>
81
+ <TableHeadCell>Name</TableHeadCell>
82
+ <TableHeadCell>Email</TableHeadCell>
83
+ <TableHeadCell>Status</TableHeadCell>
84
+ </TableHeadRow>
85
+ </TableHead>
86
+ <TableBody>
87
+ {MOCK_DATA.map((item) => (
88
+ <TableRow key={item._id}>
89
+ <TableCell>{item.name}</TableCell>
90
+ <TableCell>{item.email}</TableCell>
91
+ <TableCell>{item.status}</TableCell>
92
+ </TableRow>
93
+ ))}
94
+ </TableBody>
95
+ </Table>
96
+ </HorizontallyScrollableTableWrapper>
97
+ ),
98
+ play: async ({ canvasElement }) => {
99
+ const canvas = within(canvasElement);
100
+
101
+ await expect(canvas.getByTestId(tableTestIds.WRAPPER)).toBeInTheDocument();
102
+ await expect(canvas.getByTestId(tableTestIds.TABLE)).toBeInTheDocument();
103
+ await expect(canvas.getAllByTestId(tableTestIds.ROW).length).toBe(MOCK_DATA.length);
104
+ },
105
+ };
106
+
107
+ /** Empty state with TableEmptyRow. */
108
+ export const Empty: Story = {
109
+ render: () => (
110
+ <HorizontallyScrollableTableWrapper>
111
+ <Table>
112
+ <TableHead>
113
+ <TableHeadRow>
114
+ <TableHeadCell>Name</TableHeadCell>
115
+ <TableHeadCell>Email</TableHeadCell>
116
+ <TableHeadCell>Status</TableHeadCell>
117
+ </TableHeadRow>
118
+ </TableHead>
119
+ <TableBody>
120
+ <TableEmptyRow colSpan={3}>No data available</TableEmptyRow>
121
+ </TableBody>
122
+ </Table>
123
+ </HorizontallyScrollableTableWrapper>
124
+ ),
125
+ play: async ({ canvasElement }) => {
126
+ const canvas = within(canvasElement);
127
+
128
+ await expect(canvas.getByTestId(tableTestIds.EMPTY_ROW)).toBeInTheDocument();
129
+ await expect(canvas.queryAllByTestId(tableTestIds.ROW).length).toBe(0);
130
+ },
131
+ };
132
+
133
+ /** Loading state with TableLoadingRow. */
134
+ export const Loading: Story = {
135
+ render: () => (
136
+ <HorizontallyScrollableTableWrapper>
137
+ <Table>
138
+ <TableHead>
139
+ <TableHeadRow>
140
+ <TableHeadCell>Name</TableHeadCell>
141
+ <TableHeadCell>Email</TableHeadCell>
142
+ <TableHeadCell>Status</TableHeadCell>
143
+ </TableHeadRow>
144
+ </TableHead>
145
+ <TableBody>
146
+ <TableLoadingRow colSpan={3} />
147
+ </TableBody>
148
+ </Table>
149
+ </HorizontallyScrollableTableWrapper>
150
+ ),
151
+ play: async ({ canvasElement }) => {
152
+ const canvas = within(canvasElement);
153
+
154
+ await expect(canvas.getByTestId(tableTestIds.LOADING_ROW)).toBeInTheDocument();
155
+ },
156
+ };
157
+
158
+ /** Error state with TableErrorRow. */
159
+ export const WithError: Story = {
160
+ render: () => (
161
+ <HorizontallyScrollableTableWrapper>
162
+ <Table>
163
+ <TableHead>
164
+ <TableHeadRow>
165
+ <TableHeadCell>Name</TableHeadCell>
166
+ <TableHeadCell>Email</TableHeadCell>
167
+ <TableHeadCell>Status</TableHeadCell>
168
+ </TableHeadRow>
169
+ </TableHead>
170
+ <TableBody>
171
+ <TableErrorRow colSpan={3} error={new globalThis.Error('Connection timed out')} />
172
+ </TableBody>
173
+ </Table>
174
+ </HorizontallyScrollableTableWrapper>
175
+ ),
176
+ play: async ({ canvasElement }) => {
177
+ const canvas = within(canvasElement);
178
+
179
+ await expect(canvas.getByTestId(tableTestIds.ERROR_ROW)).toBeInTheDocument();
180
+ },
181
+ };
182
+
183
+ /** Nested links — row-level link via `to` prop + inner TableCellLink. */
184
+ export const WithNestedLinks: Story = {
185
+ render: () => (
186
+ <HorizontallyScrollableTableWrapper>
187
+ <Table>
188
+ <TableHead>
189
+ <TableHeadRow>
190
+ <TableHeadCell>Name</TableHeadCell>
191
+ <TableHeadCell>Email</TableHeadCell>
192
+ <TableHeadCell>Status</TableHeadCell>
193
+ </TableHeadRow>
194
+ </TableHead>
195
+ <TableBody>
196
+ {MOCK_DATA.map((item) => (
197
+ <TableRow key={item._id} to={`/items/${item._id}`}>
198
+ <TableCell>
199
+ <TableCellLink to={`/actors/${item._id}`}>{item.name}</TableCellLink>
200
+ </TableCell>
201
+ <TableCell>{item.email}</TableCell>
202
+ <TableCell>{item.status}</TableCell>
203
+ </TableRow>
204
+ ))}
205
+ </TableBody>
206
+ </Table>
207
+ </HorizontallyScrollableTableWrapper>
208
+ ),
209
+ play: async ({ canvasElement }) => {
210
+ const canvas = within(canvasElement);
211
+
212
+ await expect(canvas.getAllByTestId(tableTestIds.ROW).length).toBe(MOCK_DATA.length);
213
+
214
+ // Each cell in a row with `to` should have an overlay link + first cell has inner link
215
+ const cells = canvas.getAllByTestId(tableTestIds.CELL);
216
+ const firstCellLinks = cells[0].querySelectorAll('a');
217
+ await expect(firstCellLinks.length).toBe(2);
218
+ },
219
+ };
220
+
221
+ /** Direct inline data — no hooks, no data fetching. */
222
+ export const DirectData: Story = {
223
+ render: () => {
224
+ const items = [
225
+ { _id: '1', key: 'API_TOKEN', value: '••••••••', updatedAt: '2025-04-01' },
226
+ { _id: '2', key: 'BASE_URL', value: 'https://api.example.com', updatedAt: '2025-03-15' },
227
+ { _id: '3', key: 'TIMEOUT_MS', value: '30000', updatedAt: '2025-02-20' },
228
+ ];
229
+
230
+ return (
231
+ <HorizontallyScrollableTableWrapper>
232
+ <Table>
233
+ <TableHead>
234
+ <TableHeadRow>
235
+ <TableHeadCell>Key</TableHeadCell>
236
+ <TableHeadCell>Value</TableHeadCell>
237
+ <TableHeadCell>Updated</TableHeadCell>
238
+ </TableHeadRow>
239
+ </TableHead>
240
+ <TableBody>
241
+ {items.map((item) => (
242
+ <TableRow key={item._id}>
243
+ <TableCell><Text weight="bold">{item.key}</Text></TableCell>
244
+ <TableCell>{item.value}</TableCell>
245
+ <TableCell>{item.updatedAt}</TableCell>
246
+ </TableRow>
247
+ ))}
248
+ </TableBody>
249
+ </Table>
250
+ </HorizontallyScrollableTableWrapper>
251
+ );
252
+ },
253
+ play: async ({ canvasElement }) => {
254
+ const canvas = within(canvasElement);
255
+
256
+ await expect(canvas.getAllByTestId(tableTestIds.ROW).length).toBe(3);
257
+ },
258
+ };
@@ -0,0 +1,207 @@
1
+ import styled from 'styled-components';
2
+
3
+ import { theme } from '../../design_system/theme.js';
4
+ import { Box } from '../box.js';
5
+
6
+ /** z-index for sticky table head cells. Consumers can override via CSS if needed. */
7
+ const STICKY_HEAD_Z_INDEX = 5;
8
+
9
+ export const tableClassNames = {
10
+ TABLE: 'Table',
11
+ HEAD: 'Table-Head',
12
+ BODY: 'Table-Body',
13
+ FOOT: 'Table-Foot',
14
+
15
+ HEAD_ROW: 'Table-Head-Row',
16
+ HEAD_CELL: 'Table-Head-Cell',
17
+
18
+ BODY_ROW: 'Table-Body-Row',
19
+ BODY_ROW_CLICKABLE: 'Table-Body-Row_clickable',
20
+ BODY_ROW_EXPANDED: 'Table-Body-Row_expanded',
21
+ BODY_CELL: 'Table-Body-Cell',
22
+
23
+ FOOT_ROW: 'Table-Foot-Row',
24
+ FOOT_CELL: 'Table-Foot-Cell',
25
+
26
+ ROW_EMPTY: 'Table-Row-Empty',
27
+ ROW_EMPTY_CELL: 'Table-Row-Empty-Cell',
28
+
29
+ ROW_EXPANSION: 'Table-Row-Expansion',
30
+ ROW_EXPANSION_CELL: 'Table-Row-Expansion-Cell',
31
+
32
+ WRAPPER_SCROLLABLE: 'Table-Scrollable-Content',
33
+ WRAPPER_SHADOW_LEFT: 'Table-Scrollable-Shadow-Left',
34
+ WRAPPER_SHADOW_RIGHT: 'Table-Scrollable-Shadow-Right',
35
+
36
+ // Responsive hiding
37
+ HIDDEN_SM: 'Table-Cell_hiddenSM',
38
+ HIDDEN_MD: 'Table-Cell_hiddenMD',
39
+ HIDDEN_LG: 'Table-Cell_hiddenLG',
40
+
41
+ // Nested links
42
+ CELL_OVERLAY_LINK: 'Table-Cell-Overlay-Link',
43
+ CELL_LINK: 'Table-Cell-Link',
44
+ };
45
+
46
+ export const StyledTable = styled.table`
47
+ position: relative;
48
+ border-collapse: separate;
49
+ border-spacing: 0;
50
+ min-width: 100%;
51
+
52
+ /* Head cell rounding */
53
+ .${tableClassNames.HEAD_CELL}:first-child {
54
+ border-top-left-radius: ${theme.radius.radius12};
55
+ }
56
+ .${tableClassNames.HEAD_CELL}:last-child {
57
+ border-top-right-radius: ${theme.radius.radius12};
58
+ }
59
+
60
+ /* Generic cell padding */
61
+ .${tableClassNames.HEAD_CELL},
62
+ .${tableClassNames.BODY_CELL},
63
+ .${tableClassNames.ROW_EMPTY_CELL},
64
+ .${tableClassNames.ROW_EXPANSION_CELL} {
65
+ padding: ${theme.space.space8};
66
+ position: relative;
67
+ }
68
+
69
+ /* Head styling */
70
+ .${tableClassNames.HEAD_CELL} {
71
+ position: sticky;
72
+ top: 0;
73
+ z-index: ${STICKY_HEAD_Z_INDEX};
74
+ height: 4rem;
75
+ background-color: ${theme.color.neutral.backgroundMuted};
76
+ border-bottom: solid 1px ${theme.color.neutral.separatorSubtle};
77
+ color: ${theme.color.neutral.textMuted};
78
+ }
79
+
80
+ /* Body styling */
81
+ .${tableClassNames.BODY_ROW}:hover {
82
+ background-color: ${theme.color.neutral.hover};
83
+ }
84
+
85
+ .${tableClassNames.BODY_ROW}:not(:first-child) .${tableClassNames.BODY_CELL} {
86
+ border-top: solid 1px ${theme.color.neutral.separatorSubtle};
87
+ }
88
+
89
+ .${tableClassNames.BODY_ROW_CLICKABLE} {
90
+ cursor: pointer;
91
+ }
92
+
93
+ /* Foot styling */
94
+ .${tableClassNames.FOOT_ROW}:not(:first-child) .${tableClassNames.FOOT_CELL} {
95
+ border-top: solid 1px ${theme.color.neutral.separatorSubtle};
96
+ }
97
+
98
+ /* Responsive hiding */
99
+ .${tableClassNames.HIDDEN_SM} {
100
+ display: none !important;
101
+ }
102
+
103
+ @media ${theme.device.tablet} {
104
+ .${tableClassNames.HIDDEN_MD} {
105
+ display: none !important;
106
+ }
107
+ }
108
+
109
+ @media ${theme.device.desktop} {
110
+ .${tableClassNames.HIDDEN_LG} {
111
+ display: none !important;
112
+ }
113
+ }
114
+
115
+ /* Expansion row */
116
+ .${tableClassNames.ROW_EXPANSION_CELL} {
117
+ border-top: solid 1px ${theme.color.neutral.separatorSubtle};
118
+ box-shadow: inset 0rem 0.2rem 0.8rem -0.2rem ${theme.color.neutral.separatorSubtle};
119
+ }
120
+
121
+ /* Nested links — see https://www.notion.so/apify/How-to-Nested-links-4863b883e9e1498b965531d946721926 */
122
+ .${tableClassNames.CELL_OVERLAY_LINK} {
123
+ position: absolute;
124
+ top: 0;
125
+ left: 0;
126
+ width: 100%;
127
+ height: 100%;
128
+ z-index: 0;
129
+ cursor: pointer;
130
+ }
131
+
132
+ .${tableClassNames.CELL_LINK} {
133
+ position: relative;
134
+ z-index: 1;
135
+ color: ${theme.color.neutral.text};
136
+ cursor: pointer;
137
+ /* Extra padding for easier clicking, compensated by negative margin */
138
+ padding: ${theme.space.space4} ${theme.space.space8};
139
+ margin: -${theme.space.space4} -${theme.space.space8};
140
+
141
+ &:hover {
142
+ z-index: 2;
143
+ color: ${theme.color.primary.text};
144
+ }
145
+ }
146
+
147
+ /* Empty row */
148
+ .${tableClassNames.ROW_EMPTY_CELL} {
149
+ text-align: center;
150
+ padding: ${theme.space.space16};
151
+ }
152
+ `;
153
+
154
+ export const StyledHorizontallyScrollableTableWrapper = styled(Box)`
155
+ position: relative;
156
+ width: 100%;
157
+ background-color: ${theme.color.neutral.background};
158
+ border: solid 1px ${theme.color.neutral.separatorSubtle};
159
+ border-radius: ${theme.radius.radius12};
160
+ overflow: hidden;
161
+
162
+ .${tableClassNames.WRAPPER_SCROLLABLE} {
163
+ position: relative;
164
+ overflow-x: auto;
165
+ overflow-y: hidden;
166
+ }
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: 0;
173
+ bottom: 0;
174
+ width: 1.6rem;
175
+ pointer-events: none;
176
+ opacity: 0;
177
+ transition: opacity 0.3s ease;
178
+ z-index: ${STICKY_HEAD_Z_INDEX + 1};
179
+ }
180
+
181
+ &::before {
182
+ left: 0;
183
+ background: linear-gradient(to right, ${theme.color.neutral.separatorSubtle}, transparent);
184
+ }
185
+
186
+ &::after {
187
+ right: 0;
188
+ background: linear-gradient(to left, ${theme.color.neutral.separatorSubtle}, transparent);
189
+ }
190
+
191
+ &.${tableClassNames.WRAPPER_SHADOW_LEFT}::before {
192
+ opacity: 1;
193
+ }
194
+
195
+ &.${tableClassNames.WRAPPER_SHADOW_RIGHT}::after {
196
+ opacity: 1;
197
+ }
198
+ `;
199
+
200
+ export const StyledTableFooterWrapper = styled(Box)`
201
+ position: relative;
202
+ width: 100%;
203
+ background-color: ${theme.color.neutral.backgroundMuted};
204
+ border-top: solid 1px ${theme.color.neutral.separatorSubtle};
205
+ overflow-x: auto;
206
+ scrollbar-width: none;
207
+ `;
@@ -0,0 +1,9 @@
1
+ import clsx from 'clsx';
2
+ import type { FC, HTMLAttributes } from 'react';
3
+
4
+ import { StyledTable, tableClassNames } from './table.styled.js';
5
+ import { tableTestIds } from './table_test_ids.js';
6
+
7
+ export const Table: FC<HTMLAttributes<HTMLTableElement>> = ({ className, ...rest }) => (
8
+ <StyledTable data-test={tableTestIds.TABLE} className={clsx(tableClassNames.TABLE, className)} {...rest} />
9
+ );
@@ -0,0 +1,9 @@
1
+ import clsx from 'clsx';
2
+ import type { FC, HTMLAttributes } from 'react';
3
+
4
+ import { tableClassNames } from './table.styled.js';
5
+ import { tableTestIds } from './table_test_ids.js';
6
+
7
+ export const TableBody: FC<HTMLAttributes<HTMLTableSectionElement>> = ({ className, ...rest }) => (
8
+ <tbody data-test={tableTestIds.BODY} className={clsx(tableClassNames.BODY, className)} {...rest} />
9
+ );
@@ -0,0 +1,28 @@
1
+ import clsx from 'clsx';
2
+ import type { FC, TdHTMLAttributes } from 'react';
3
+ import { useContext } from 'react';
4
+
5
+ import { Link, type LinkProps } from '../link.js';
6
+ import { TableRowLinkContext } from './table.context.js';
7
+ import { tableClassNames } from './table.styled.js';
8
+ import { tableTestIds } from './table_test_ids.js';
9
+
10
+ export const TableCell: FC<TdHTMLAttributes<HTMLTableCellElement>> = ({ className, children, ...rest }) => {
11
+ const rowLink = useContext(TableRowLinkContext);
12
+
13
+ return (
14
+ <td data-test={tableTestIds.CELL} className={clsx(tableClassNames.BODY_CELL, className)} {...rest}>
15
+ {rowLink && <Link className={tableClassNames.CELL_OVERLAY_LINK} to={rowLink} tabIndex={-1} ariaHidden />}
16
+ {children}
17
+ </td>
18
+ );
19
+ };
20
+
21
+ /**
22
+ * Inner link inside a cell whose row has a `to` prop (overlay link).
23
+ * Elevated above the row overlay via `z-index` so it remains clickable and hoverable.
24
+ * Includes extra padding (compensated by negative margin) for easier click targets.
25
+ */
26
+ export const TableCellLink: FC<LinkProps> = ({ className, ...rest }) => (
27
+ <Link className={clsx(tableClassNames.CELL_LINK, className)} {...rest} />
28
+ );
@@ -0,0 +1,12 @@
1
+ import type { FC, ReactNode } from 'react';
2
+
3
+ import { tableClassNames } from './table.styled.js';
4
+ import { tableTestIds } from './table_test_ids.js';
5
+
6
+ export const TableEmptyRow: FC<{ colSpan: number; children: ReactNode }> = ({ colSpan, children }) => (
7
+ <tr data-test={tableTestIds.EMPTY_ROW} className={tableClassNames.ROW_EMPTY}>
8
+ <td colSpan={colSpan} className={tableClassNames.ROW_EMPTY_CELL}>
9
+ {children}
10
+ </td>
11
+ </tr>
12
+ );
@@ -0,0 +1,13 @@
1
+ import type { FC, ReactNode } from 'react';
2
+
3
+ import { Text } from '../text/index.js';
4
+ import { tableClassNames } from './table.styled.js';
5
+ import { tableTestIds } from './table_test_ids.js';
6
+
7
+ export const TableErrorRow: FC<{ colSpan: number; children?: ReactNode; error?: Error | null }> = ({ colSpan, children, error }) => (
8
+ <tr data-test={tableTestIds.ERROR_ROW} className={tableClassNames.ROW_EMPTY}>
9
+ <td colSpan={colSpan} className={tableClassNames.ROW_EMPTY_CELL}>
10
+ {children ?? <Text color="error">{error?.message ?? 'Failed to load data'}</Text>}
11
+ </td>
12
+ </tr>
13
+ );
@@ -0,0 +1,24 @@
1
+ import clsx from 'clsx';
2
+ import type { FC, ReactNode } from 'react';
3
+
4
+ import { tableClassNames } from './table.styled.js';
5
+ import { tableTestIds } from './table_test_ids.js';
6
+ import type { TableExpansion } from './types.js';
7
+
8
+ export const TableExpansionRow: FC<{
9
+ colSpan: number;
10
+ children: ReactNode;
11
+ expansion: TableExpansion;
12
+ itemId: string;
13
+ className?: string;
14
+ }> = ({ colSpan, children, expansion, itemId, className }) => {
15
+ if (!expansion.isExpanded(itemId)) return null;
16
+
17
+ return (
18
+ <tr data-test={tableTestIds.EXPANSION_ROW} className={clsx(tableClassNames.ROW_EXPANSION, className)}>
19
+ <td colSpan={colSpan} className={tableClassNames.ROW_EXPANSION_CELL}>
20
+ {children}
21
+ </td>
22
+ </tr>
23
+ );
24
+ };
@@ -0,0 +1,9 @@
1
+ import clsx from 'clsx';
2
+ import type { FC, HTMLAttributes } from 'react';
3
+
4
+ import { tableClassNames } from './table.styled.js';
5
+ import { tableTestIds } from './table_test_ids.js';
6
+
7
+ export const TableFoot: FC<HTMLAttributes<HTMLTableSectionElement>> = ({ className, ...rest }) => (
8
+ <tfoot data-test={tableTestIds.FOOT} className={clsx(tableClassNames.FOOT, className)} {...rest} />
9
+ );