@adecore/ui 0.61.0 → 0.62.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/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/locales/en.d.ts +3 -0
- package/dist/locales/en.js +3 -0
- package/dist/locales/nl.d.ts +3 -0
- package/dist/locales/nl.js +3 -0
- package/dist/table/index.parts.d.ts +1 -0
- package/dist/table/index.parts.js +1 -0
- package/dist/table/parts.d.ts +50 -0
- package/dist/table/parts.js +1075 -0
- package/dist/theme.css +8 -0
- package/package.json +1 -1
- package/src/index.ts +16 -0
- package/src/locales/en.ts +3 -0
- package/src/locales/nl.ts +3 -0
- package/src/table/index.parts.ts +11 -0
- package/src/table/parts.tsx +424 -0
- package/src/theme.css +8 -0
package/dist/theme.css
CHANGED
|
@@ -1118,6 +1118,14 @@
|
|
|
1118
1118
|
.middle-truncate-middle { display: inline-block; width: 0; overflow: hidden; vertical-align: top; }
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
1121
|
+
@layer components {
|
|
1122
|
+
/* `Table`. A line above every row of the body but the first, also when that row opens a group, so
|
|
1123
|
+
no line doubles the head's edge or the frame's. */
|
|
1124
|
+
.ade-table-line { border-top: 1px solid var(--border-soft); }
|
|
1125
|
+
:is(.ade-table-grid > :first-child, .ade-table-head + *):is(.ade-table-line),
|
|
1126
|
+
:is(.ade-table-grid > :first-child, .ade-table-head + *) > .ade-table-line:first-child { border-top-width: 0; }
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1121
1129
|
@layer components {
|
|
1122
1130
|
/* `ScrollArea` and `useOverlayScrollbars`. The scroller stays native and hides its own bar; thin bars
|
|
1123
1131
|
float over the content, hidden at rest and shown while it scrolls. */
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * as KeyValueList from './key-value-list/index.parts.ts';
|
|
|
14
14
|
export * as Menu from './menu/index.parts.ts';
|
|
15
15
|
export * as Popover from './popover/index.parts.ts';
|
|
16
16
|
export * as PreviewCard from './preview-card/index.parts.ts';
|
|
17
|
+
export * as Table from './table/index.parts.ts';
|
|
17
18
|
export * as Tabs from './tabs/index.parts.ts';
|
|
18
19
|
|
|
19
20
|
export type { ComboboxInputProps, ComboboxItemProps, ComboboxPopupProps } from './combobox/parts.tsx';
|
|
@@ -32,6 +33,21 @@ export type {
|
|
|
32
33
|
|
|
33
34
|
export type { KeyValueListItemProps, KeyValueListNameProps, KeyValueListRootProps, KeyValueListValueProps } from './key-value-list/parts.tsx';
|
|
34
35
|
export type { PopoverPopupProps, PopupVariant, PreviewCardPopupProps } from './popover/parts.tsx';
|
|
36
|
+
export type {
|
|
37
|
+
TableBreakpoint,
|
|
38
|
+
TableCellProps,
|
|
39
|
+
TableColumnLayout,
|
|
40
|
+
TableColumnProps,
|
|
41
|
+
TableDensity,
|
|
42
|
+
TableEmptyProps,
|
|
43
|
+
TableFootProps,
|
|
44
|
+
TableGroupProps,
|
|
45
|
+
TableHeadProps,
|
|
46
|
+
TableLoadingProps,
|
|
47
|
+
TableRootProps,
|
|
48
|
+
TableRowProps,
|
|
49
|
+
TableSort
|
|
50
|
+
} from './table/parts.tsx';
|
|
35
51
|
export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue, TabsVariant } from './tabs/parts.tsx';
|
|
36
52
|
export { tabDropIndex, useTabDrag, type TabDrag, type TabDragOptions } from './tabs/use-tab-drag.ts';
|
|
37
53
|
|
package/src/locales/en.ts
CHANGED
package/src/locales/nl.ts
CHANGED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-react';
|
|
4
|
+
import {
|
|
5
|
+
Children,
|
|
6
|
+
createContext,
|
|
7
|
+
useContext,
|
|
8
|
+
useId,
|
|
9
|
+
type ComponentProps,
|
|
10
|
+
type CSSProperties,
|
|
11
|
+
type KeyboardEvent,
|
|
12
|
+
type MouseEvent,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
type Ref
|
|
15
|
+
} from 'react';
|
|
16
|
+
import { useTranslation } from 'react-i18next';
|
|
17
|
+
import { Icon } from '../Icon.tsx';
|
|
18
|
+
import { ScrollArea } from '../ScrollArea.tsx';
|
|
19
|
+
import { Skeleton } from '../Skeleton.tsx';
|
|
20
|
+
|
|
21
|
+
/* The container sizes of the theme, measured on the table itself rather than the window, so a table in a narrow dialog drops its columns too. */
|
|
22
|
+
export type TableBreakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
23
|
+
|
|
24
|
+
export type TableSort = 'none' | 'asc' | 'desc';
|
|
25
|
+
export type TableDensity = 'regular' | 'compact';
|
|
26
|
+
|
|
27
|
+
export interface TableColumnLayout {
|
|
28
|
+
/* A grid track: `minmax(0,2fr)`, `auto`, `120px`. */
|
|
29
|
+
width: string;
|
|
30
|
+
|
|
31
|
+
/* `end` for numbers, which then line up in tabular figures. Every cell of the column follows it. */
|
|
32
|
+
align?: 'start' | 'end';
|
|
33
|
+
|
|
34
|
+
/* The column and its cells leave while the table is narrower than this. */
|
|
35
|
+
hideBelow?: TableBreakpoint;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const BREAKPOINTS: readonly TableBreakpoint[] = ['sm', 'md', 'lg', 'xl', '2xl'];
|
|
39
|
+
|
|
40
|
+
/* A narrower query comes later in the CSS, so it wins over a wider one. */
|
|
41
|
+
const TEMPLATE_BELOW = {
|
|
42
|
+
sm: '@max-sm:grid-cols-(--table-columns-sm)',
|
|
43
|
+
md: '@max-md:grid-cols-(--table-columns-md)',
|
|
44
|
+
lg: '@max-lg:grid-cols-(--table-columns-lg)',
|
|
45
|
+
xl: '@max-xl:grid-cols-(--table-columns-xl)',
|
|
46
|
+
'2xl': '@max-2xl:grid-cols-(--table-columns-2xl)'
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
const HIDDEN_BELOW = {
|
|
50
|
+
sm: '@max-sm:hidden',
|
|
51
|
+
md: '@max-md:hidden',
|
|
52
|
+
lg: '@max-lg:hidden',
|
|
53
|
+
xl: '@max-xl:hidden',
|
|
54
|
+
'2xl': '@max-2xl:hidden'
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
const ROW_HEIGHT = { regular: 'min-h-10', compact: 'min-h-8' } as const;
|
|
58
|
+
const CELL_PADDING = { regular: 'py-2', compact: 'py-1' } as const;
|
|
59
|
+
const TEXT_SIZE = { regular: 'text-sm', compact: 'text-xs' } as const;
|
|
60
|
+
const ARIA_SORT = { none: 'none', asc: 'ascending', desc: 'descending' } as const;
|
|
61
|
+
const SKELETON_WIDTHS = ['w-3/4', 'w-1/2', 'w-2/3'] as const;
|
|
62
|
+
|
|
63
|
+
/* Every row is a subgrid of the table, so its cells line up with the columns and it still has a box to tint and focus. */
|
|
64
|
+
const ROW = 'col-span-full grid grid-cols-subgrid items-center px-4.5';
|
|
65
|
+
|
|
66
|
+
/* A click that lands on one of these inside a row is the control's, not the row's. */
|
|
67
|
+
const CONTROLS =
|
|
68
|
+
'a[href], button, input, select, textarea, label, [contenteditable="true"], [role="button"], [role="checkbox"], [role="switch"], [role="menuitem"], [role="option"]';
|
|
69
|
+
|
|
70
|
+
interface TableContextValue {
|
|
71
|
+
columns: readonly TableColumnLayout[];
|
|
72
|
+
density: TableDensity;
|
|
73
|
+
scrolls: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const TableContext = createContext<TableContextValue>({ columns: [], density: 'regular', scrolls: false });
|
|
77
|
+
const ColumnIndexContext = createContext(-1);
|
|
78
|
+
|
|
79
|
+
const templateOf = (columns: readonly TableColumnLayout[]): string => columns.map((column) => column.width).join(' ');
|
|
80
|
+
|
|
81
|
+
/* Below a breakpoint every column that hides at it or at a wider one is gone. */
|
|
82
|
+
const visibleBelow = (columns: readonly TableColumnLayout[], breakpoint: TableBreakpoint): TableColumnLayout[] =>
|
|
83
|
+
columns.filter((column) => column.hideBelow === undefined || BREAKPOINTS.indexOf(column.hideBelow) < BREAKPOINTS.indexOf(breakpoint));
|
|
84
|
+
|
|
85
|
+
/* A cell knows its column by its place in the row, so cells are the row's direct children. */
|
|
86
|
+
const indexed = (children: ReactNode): ReactNode => Children.map(children, (child, index) => <ColumnIndexContext value={index}>{child}</ColumnIndexContext>);
|
|
87
|
+
|
|
88
|
+
const columnClass = (column: TableColumnLayout | undefined): string =>
|
|
89
|
+
clsx(
|
|
90
|
+
'flex min-w-0 items-center gap-2',
|
|
91
|
+
column?.align === 'end' && 'justify-end text-end tabular-nums',
|
|
92
|
+
column?.hideBelow && HIDDEN_BELOW[column.hideBelow]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
function useColumn(): TableColumnLayout | undefined {
|
|
96
|
+
const { columns } = useContext(TableContext);
|
|
97
|
+
|
|
98
|
+
return columns[useContext(ColumnIndexContext)];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type TableRootProps = Omit<ComponentProps<'div'>, 'role' | 'aria-label'> & {
|
|
102
|
+
/* The accessible name. */
|
|
103
|
+
label: string;
|
|
104
|
+
|
|
105
|
+
columns: readonly TableColumnLayout[];
|
|
106
|
+
density?: TableDensity;
|
|
107
|
+
|
|
108
|
+
/* A border with rounded corners around the table. Without it the table is bare, to sit inside a card that already draws one. */
|
|
109
|
+
framed?: boolean;
|
|
110
|
+
|
|
111
|
+
/* Pixels the table grows to before its body scrolls under a head that stays. */
|
|
112
|
+
maxHeight?: number;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/* A grid rather than a `<table>`, so a cell holds any control and a column can leave on a narrow table. */
|
|
116
|
+
export function TableRoot({ label, columns, density = 'regular', framed = false, maxHeight, className, children, ...props }: TableRootProps) {
|
|
117
|
+
const scrolls = maxHeight !== undefined;
|
|
118
|
+
const used = BREAKPOINTS.filter((breakpoint) => columns.some((column) => column.hideBelow === breakpoint));
|
|
119
|
+
const tracks: Record<string, string> = { '--table-columns': templateOf(columns) };
|
|
120
|
+
|
|
121
|
+
for (const breakpoint of used) {
|
|
122
|
+
tracks[`--table-columns-${breakpoint}`] = templateOf(visibleBelow(columns, breakpoint));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const grid = clsx(
|
|
126
|
+
'ade-table-grid grid grid-cols-(--table-columns) gap-x-3',
|
|
127
|
+
used.map((breakpoint) => TEMPLATE_BELOW[breakpoint]),
|
|
128
|
+
TEXT_SIZE[density]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<TableContext value={{ columns, density, scrolls }}>
|
|
133
|
+
<div
|
|
134
|
+
role="table"
|
|
135
|
+
aria-label={label}
|
|
136
|
+
{...props}
|
|
137
|
+
className={clsx('@container min-w-0 text-text', framed && 'overflow-clip rounded-lg border border-border bg-surface', className)}
|
|
138
|
+
>
|
|
139
|
+
{scrolls ? (
|
|
140
|
+
<ScrollArea viewportClassName={grid} style={{ ...tracks, maxHeight } as CSSProperties}>
|
|
141
|
+
{children}
|
|
142
|
+
</ScrollArea>
|
|
143
|
+
) : (
|
|
144
|
+
<div className={grid} style={tracks as CSSProperties}>
|
|
145
|
+
{children}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</TableContext>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type TableHeadProps = useRender.ComponentProps<'div'>;
|
|
154
|
+
|
|
155
|
+
/* The row of column headers. It stays at the top while the body scrolls, on the surface color. */
|
|
156
|
+
export function TableHead({ render, className, ref, children, ...props }: TableHeadProps) {
|
|
157
|
+
const { density, scrolls } = useContext(TableContext);
|
|
158
|
+
|
|
159
|
+
return useRender({
|
|
160
|
+
render,
|
|
161
|
+
ref,
|
|
162
|
+
defaultTagName: 'div',
|
|
163
|
+
props: {
|
|
164
|
+
role: 'rowgroup',
|
|
165
|
+
...props,
|
|
166
|
+
className: clsx('ade-table-head col-span-full grid grid-cols-subgrid border-b border-border', scrolls && 'sticky top-0 z-1 bg-surface', className),
|
|
167
|
+
children: (
|
|
168
|
+
<div role="row" className={clsx(ROW, density === 'compact' ? 'min-h-7' : 'min-h-8', 'text-2xs font-medium text-text-muted')}>
|
|
169
|
+
{indexed(children)}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type TableColumnProps = useRender.ComponentProps<'div'> & {
|
|
177
|
+
/* Makes the header a button. The caller sorts the rows. */
|
|
178
|
+
sort?: TableSort;
|
|
179
|
+
|
|
180
|
+
/* The direction a click asks for: the other one, or ascending from `none`. */
|
|
181
|
+
onSort?(direction: 'asc' | 'desc'): void;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export function TableColumn({ sort, onSort, render, className, ref, children, ...props }: TableColumnProps) {
|
|
185
|
+
const column = useColumn();
|
|
186
|
+
const label = <span className="truncate">{children}</span>;
|
|
187
|
+
|
|
188
|
+
return useRender({
|
|
189
|
+
render,
|
|
190
|
+
ref,
|
|
191
|
+
defaultTagName: 'div',
|
|
192
|
+
props: {
|
|
193
|
+
role: 'columnheader',
|
|
194
|
+
'aria-sort': sort === undefined ? undefined : ARIA_SORT[sort],
|
|
195
|
+
...props,
|
|
196
|
+
className: clsx(columnClass(column), 'py-1.5', className),
|
|
197
|
+
children:
|
|
198
|
+
sort === undefined ? (
|
|
199
|
+
label
|
|
200
|
+
) : (
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
className="-mx-1 inline-flex min-w-0 items-center gap-1 rounded-sm px-1 hover:text-text"
|
|
204
|
+
onClick={() => onSort?.(sort === 'asc' ? 'desc' : 'asc')}
|
|
205
|
+
>
|
|
206
|
+
{label}
|
|
207
|
+
<Icon
|
|
208
|
+
icon={sort === 'asc' ? ArrowUp : sort === 'desc' ? ArrowDown : ChevronsUpDown}
|
|
209
|
+
size={12}
|
|
210
|
+
className={clsx('shrink-0', sort === 'none' && 'text-text-faint')}
|
|
211
|
+
/>
|
|
212
|
+
</button>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type TableRowProps = useRender.ComponentProps<'div'> & {
|
|
219
|
+
/* Tinted in the accent, for a row a person picked. */
|
|
220
|
+
selected?: boolean;
|
|
221
|
+
|
|
222
|
+
/* Muted text, for a row a person reads but cannot change. */
|
|
223
|
+
muted?: boolean;
|
|
224
|
+
|
|
225
|
+
/* `false` keeps a row with `onClick` out of the tab order, for a row whose own control (a checkbox) already does what its click does. */
|
|
226
|
+
focusable?: boolean;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const fromControl = (event: MouseEvent<HTMLElement>): boolean => {
|
|
230
|
+
const row = event.currentTarget;
|
|
231
|
+
const target = event.target;
|
|
232
|
+
|
|
233
|
+
// React bubbles a click out of a portal, such as a menu opened from a cell, into the row.
|
|
234
|
+
if (!(target instanceof Element) || !row.contains(target)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const control = target.closest(CONTROLS);
|
|
239
|
+
|
|
240
|
+
return control !== null && control !== row && row.contains(control);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/* With `onClick` the row is a tab stop that Enter and Space press, unless `focusable` is false, and a click on a control inside it stays the control's. */
|
|
244
|
+
export function TableRow({ selected = false, muted = false, focusable = true, onClick, onKeyDown, render, className, ref, children, ...props }: TableRowProps) {
|
|
245
|
+
const { density } = useContext(TableContext);
|
|
246
|
+
const clickable = onClick !== undefined;
|
|
247
|
+
const pressable = clickable && focusable;
|
|
248
|
+
|
|
249
|
+
return useRender({
|
|
250
|
+
render,
|
|
251
|
+
ref,
|
|
252
|
+
defaultTagName: 'div',
|
|
253
|
+
props: {
|
|
254
|
+
role: 'row',
|
|
255
|
+
tabIndex: pressable ? 0 : undefined,
|
|
256
|
+
...props,
|
|
257
|
+
'data-selected': selected ? '' : undefined,
|
|
258
|
+
className: clsx(
|
|
259
|
+
'ade-table-line',
|
|
260
|
+
ROW,
|
|
261
|
+
ROW_HEIGHT[density],
|
|
262
|
+
selected ? 'bg-accent-soft' : clickable && 'hover:bg-surface-hover',
|
|
263
|
+
pressable && 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent-on-surface',
|
|
264
|
+
muted && 'text-text-muted',
|
|
265
|
+
className
|
|
266
|
+
),
|
|
267
|
+
onClick: clickable
|
|
268
|
+
? (event: MouseEvent<HTMLDivElement>) => {
|
|
269
|
+
if (!fromControl(event)) {
|
|
270
|
+
onClick(event);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
: undefined,
|
|
274
|
+
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => {
|
|
275
|
+
onKeyDown?.(event);
|
|
276
|
+
|
|
277
|
+
if (!pressable || event.defaultPrevented || event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
event.currentTarget.click();
|
|
283
|
+
},
|
|
284
|
+
children: indexed(children)
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export type TableCellProps = useRender.ComponentProps<'div'> & {
|
|
290
|
+
/* Text that does not fit ends in an ellipsis; `false` wraps it. Only text truncates: a control or a block of the caller's keeps its own width. */
|
|
291
|
+
truncate?: boolean;
|
|
292
|
+
|
|
293
|
+
/* Monospace secondary text, for an id, a path or a hostname. */
|
|
294
|
+
mono?: boolean;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const isText = (children: ReactNode): boolean =>
|
|
298
|
+
typeof children === 'string' ||
|
|
299
|
+
typeof children === 'number' ||
|
|
300
|
+
(Array.isArray(children) && children.every((child) => typeof child === 'string' || typeof child === 'number'));
|
|
301
|
+
|
|
302
|
+
/* Takes its column's alignment and hiding. Any content may sit inside: a pill, a switch, a checkbox, an input or a menu button. */
|
|
303
|
+
export function TableCell({ truncate = true, mono = false, render, className, ref, children, ...props }: TableCellProps) {
|
|
304
|
+
const column = useColumn();
|
|
305
|
+
const { density } = useContext(TableContext);
|
|
306
|
+
|
|
307
|
+
return useRender({
|
|
308
|
+
render,
|
|
309
|
+
ref,
|
|
310
|
+
defaultTagName: 'div',
|
|
311
|
+
props: {
|
|
312
|
+
role: 'cell',
|
|
313
|
+
...props,
|
|
314
|
+
className: clsx(columnClass(column), CELL_PADDING[density], !truncate && 'flex-wrap', mono && 'font-mono text-code text-text-muted', className),
|
|
315
|
+
children: isText(children) ? <span className={truncate ? 'min-w-0 truncate' : 'min-w-0 break-words'}>{children}</span> : children
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export type TableGroupProps = useRender.ComponentProps<'div'> & {
|
|
321
|
+
/* The row above the group's rows, spanning every column. */
|
|
322
|
+
label: ReactNode;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export function TableGroup({ label, render, className, ref, children, ...props }: TableGroupProps) {
|
|
326
|
+
const { columns } = useContext(TableContext);
|
|
327
|
+
const id = useId();
|
|
328
|
+
|
|
329
|
+
return useRender({
|
|
330
|
+
render,
|
|
331
|
+
ref,
|
|
332
|
+
defaultTagName: 'div',
|
|
333
|
+
props: {
|
|
334
|
+
role: 'rowgroup',
|
|
335
|
+
'aria-labelledby': id,
|
|
336
|
+
...props,
|
|
337
|
+
className: clsx('col-span-full grid grid-cols-subgrid', className),
|
|
338
|
+
children: (
|
|
339
|
+
<>
|
|
340
|
+
<div role="row" className="ade-table-line col-span-full grid min-h-8 grid-cols-subgrid items-end px-4.5 pt-3 pb-1 text-2xs text-text-faint">
|
|
341
|
+
<div role="cell" id={id} aria-colspan={columns.length} className="col-span-full min-w-0 truncate">
|
|
342
|
+
{label}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
{children}
|
|
346
|
+
</>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export type TableFootProps = useRender.ComponentProps<'div'>;
|
|
353
|
+
|
|
354
|
+
/* The totals under the body, behind a stronger line. Its cells follow the columns like a row's. */
|
|
355
|
+
export function TableFoot({ render, className, ref, children, ...props }: TableFootProps) {
|
|
356
|
+
const { density } = useContext(TableContext);
|
|
357
|
+
|
|
358
|
+
return useRender({
|
|
359
|
+
render,
|
|
360
|
+
ref,
|
|
361
|
+
defaultTagName: 'div',
|
|
362
|
+
props: {
|
|
363
|
+
role: 'rowgroup',
|
|
364
|
+
...props,
|
|
365
|
+
className: clsx('col-span-full grid grid-cols-subgrid border-t border-border font-medium', className),
|
|
366
|
+
children: (
|
|
367
|
+
<div role="row" className={clsx(ROW, ROW_HEIGHT[density])}>
|
|
368
|
+
{indexed(children)}
|
|
369
|
+
</div>
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export type TableEmptyProps = useRender.ComponentProps<'div'>;
|
|
376
|
+
|
|
377
|
+
/* The one row of a table without rows, its message across every column. */
|
|
378
|
+
export function TableEmpty({ render, className, ref, children, ...props }: TableEmptyProps) {
|
|
379
|
+
const { columns } = useContext(TableContext);
|
|
380
|
+
|
|
381
|
+
return useRender({
|
|
382
|
+
render,
|
|
383
|
+
ref,
|
|
384
|
+
defaultTagName: 'div',
|
|
385
|
+
props: {
|
|
386
|
+
role: 'row',
|
|
387
|
+
...props,
|
|
388
|
+
className: clsx('ade-table-line', ROW, className),
|
|
389
|
+
children: (
|
|
390
|
+
<div role="cell" aria-colspan={columns.length} className="col-span-full min-w-0 py-6 text-center break-words text-text-muted">
|
|
391
|
+
{children}
|
|
392
|
+
</div>
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface TableLoadingProps {
|
|
399
|
+
/* How many rows of bars stand in for the rows on their way. */
|
|
400
|
+
rows?: number;
|
|
401
|
+
|
|
402
|
+
className?: string;
|
|
403
|
+
ref?: Ref<HTMLDivElement>;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Rows of bars under the columns; a screen reader hears that the table is loading instead. */
|
|
407
|
+
export function TableLoading({ rows = 3, className, ref }: TableLoadingProps) {
|
|
408
|
+
const { t } = useTranslation('ui');
|
|
409
|
+
const { columns } = useContext(TableContext);
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<div ref={ref} role="rowgroup" aria-busy aria-label={t('table.loading')} className={clsx('col-span-full grid grid-cols-subgrid', className)}>
|
|
413
|
+
{Array.from({ length: rows }, (_, row) => (
|
|
414
|
+
<TableRow key={row} aria-hidden>
|
|
415
|
+
{Array.from(columns, (_, index) => (
|
|
416
|
+
<TableCell key={index}>
|
|
417
|
+
<Skeleton className={SKELETON_WIDTHS[(row + index) % SKELETON_WIDTHS.length]} />
|
|
418
|
+
</TableCell>
|
|
419
|
+
))}
|
|
420
|
+
</TableRow>
|
|
421
|
+
))}
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
package/src/theme.css
CHANGED
|
@@ -1118,6 +1118,14 @@
|
|
|
1118
1118
|
.middle-truncate-middle { display: inline-block; width: 0; overflow: hidden; vertical-align: top; }
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
1121
|
+
@layer components {
|
|
1122
|
+
/* `Table`. A line above every row of the body but the first, also when that row opens a group, so
|
|
1123
|
+
no line doubles the head's edge or the frame's. */
|
|
1124
|
+
.ade-table-line { border-top: 1px solid var(--border-soft); }
|
|
1125
|
+
:is(.ade-table-grid > :first-child, .ade-table-head + *):is(.ade-table-line),
|
|
1126
|
+
:is(.ade-table-grid > :first-child, .ade-table-head + *) > .ade-table-line:first-child { border-top-width: 0; }
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1121
1129
|
@layer components {
|
|
1122
1130
|
/* `ScrollArea` and `useOverlayScrollbars`. The scroller stays native and hides its own bar; thin bars
|
|
1123
1131
|
float over the content, hidden at rest and shown while it scrolls. */
|