@kennycorrea/ui 0.11.4 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -0
- package/README.md +251 -1
- package/dist/atoms/ActionMenu/ActionMenu.d.ts +37 -0
- package/dist/atoms/ActionMenu/ActionMenu.js +28 -0
- package/dist/atoms/ActionMenu/index.d.ts +1 -0
- package/dist/atoms/ActionMenu/index.js +1 -0
- package/dist/atoms/ActionSheet/ActionSheet.d.ts +10 -4
- package/dist/atoms/ActionSheet/ActionSheet.js +6 -15
- package/dist/atoms/ActionSheet/confirm.d.ts +1 -1
- package/dist/atoms/ActionSheet/confirm.js +1 -1
- package/dist/atoms/Avatar/Avatar.d.ts +1 -1
- package/dist/atoms/Badge/Badge.js +4 -4
- package/dist/atoms/Button/Button.js +2 -3
- package/dist/atoms/Button/Button.types.d.ts +5 -8
- package/dist/atoms/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/atoms/ContextMenu/ContextMenu.js +1 -1
- package/dist/atoms/Icon/Icon.d.ts +7 -9
- package/dist/atoms/Icon/Icon.js +23 -8
- package/dist/atoms/Pill/Pill.d.ts +17 -0
- package/dist/atoms/Pill/Pill.js +12 -0
- package/dist/atoms/Pill/index.d.ts +1 -0
- package/dist/atoms/Pill/index.js +1 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +9 -2
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +6 -15
- package/dist/atoms/StatusIndicator/StatusIndicator.d.ts +14 -0
- package/dist/atoms/StatusIndicator/StatusIndicator.js +8 -0
- package/dist/atoms/StatusIndicator/index.d.ts +1 -0
- package/dist/atoms/StatusIndicator/index.js +1 -0
- package/dist/atoms/TextLink/TextLink.d.ts +36 -0
- package/dist/atoms/TextLink/TextLink.js +17 -0
- package/dist/atoms/TextLink/index.d.ts +1 -0
- package/dist/atoms/TextLink/index.js +1 -0
- package/dist/atoms/Toast/Toast.d.ts +1 -1
- package/dist/atoms/Toast/Toast.js +1 -1
- package/dist/atoms/index.d.ts +4 -0
- package/dist/atoms/index.js +4 -0
- package/dist/hooks/breakpoints.d.ts +20 -0
- package/dist/hooks/breakpoints.js +13 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/useControllableState.d.ts +17 -0
- package/dist/hooks/useControllableState.js +22 -0
- package/dist/hooks/useElementWidth.d.ts +7 -0
- package/dist/hooks/useElementWidth.js +22 -0
- package/dist/hooks/useMediaQuery.d.ts +18 -0
- package/dist/hooks/useMediaQuery.js +51 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/molecules/AccentPicker/AccentPicker.d.ts +30 -0
- package/dist/molecules/AccentPicker/AccentPicker.js +27 -0
- package/dist/molecules/AccentPicker/index.d.ts +1 -0
- package/dist/molecules/AccentPicker/index.js +1 -0
- package/dist/molecules/Breadcrumbs/Breadcrumbs.d.ts +27 -0
- package/dist/molecules/Breadcrumbs/Breadcrumbs.js +40 -0
- package/dist/molecules/Breadcrumbs/index.d.ts +1 -0
- package/dist/molecules/Breadcrumbs/index.js +1 -0
- package/dist/molecules/Calendar/Calendar.d.ts +42 -0
- package/dist/molecules/Calendar/Calendar.js +111 -0
- package/dist/molecules/Calendar/index.d.ts +1 -0
- package/dist/molecules/Calendar/index.js +1 -0
- package/dist/molecules/DateInput/DateInput.d.ts +35 -0
- package/dist/molecules/DateInput/DateInput.js +41 -0
- package/dist/molecules/DateInput/index.d.ts +1 -0
- package/dist/molecules/DateInput/index.js +1 -0
- package/dist/molecules/EmptyState/EmptyState.d.ts +19 -0
- package/dist/molecules/EmptyState/EmptyState.js +7 -0
- package/dist/molecules/EmptyState/index.d.ts +1 -0
- package/dist/molecules/EmptyState/index.js +1 -0
- package/dist/molecules/ErrorState/ErrorState.d.ts +18 -0
- package/dist/molecules/ErrorState/ErrorState.js +12 -0
- package/dist/molecules/ErrorState/index.d.ts +1 -0
- package/dist/molecules/ErrorState/index.js +1 -0
- package/dist/molecules/ErrorSummary/ErrorSummary.d.ts +25 -0
- package/dist/molecules/ErrorSummary/ErrorSummary.js +32 -0
- package/dist/molecules/ErrorSummary/index.d.ts +1 -0
- package/dist/molecules/ErrorSummary/index.js +1 -0
- package/dist/molecules/FilterChips/FilterChips.d.ts +10 -0
- package/dist/molecules/FilterChips/FilterChips.js +13 -0
- package/dist/molecules/FilterChips/index.d.ts +1 -0
- package/dist/molecules/FilterChips/index.js +1 -0
- package/dist/molecules/FilterPopover/FilterPopover.d.ts +27 -0
- package/dist/molecules/FilterPopover/FilterPopover.js +32 -0
- package/dist/molecules/FilterPopover/index.d.ts +1 -0
- package/dist/molecules/FilterPopover/index.js +1 -0
- package/dist/molecules/FormActions/FormActions.d.ts +16 -0
- package/dist/molecules/FormActions/FormActions.js +5 -0
- package/dist/molecules/FormActions/index.d.ts +1 -0
- package/dist/molecules/FormActions/index.js +1 -0
- package/dist/molecules/FormField/FormField.d.ts +52 -0
- package/dist/molecules/FormField/FormField.js +63 -0
- package/dist/molecules/FormField/index.d.ts +1 -0
- package/dist/molecules/FormField/index.js +1 -0
- package/dist/molecules/FormSection/FormSection.d.ts +14 -0
- package/dist/molecules/FormSection/FormSection.js +10 -0
- package/dist/molecules/FormSection/index.d.ts +1 -0
- package/dist/molecules/FormSection/index.js +1 -0
- package/dist/molecules/KeyValueList/KeyValueList.d.ts +22 -0
- package/dist/molecules/KeyValueList/KeyValueList.js +9 -0
- package/dist/molecules/KeyValueList/index.d.ts +1 -0
- package/dist/molecules/KeyValueList/index.js +1 -0
- package/dist/molecules/LoadingState/LoadingState.d.ts +20 -0
- package/dist/molecules/LoadingState/LoadingState.js +22 -0
- package/dist/molecules/LoadingState/index.d.ts +1 -0
- package/dist/molecules/LoadingState/index.js +1 -0
- package/dist/molecules/NavItem/NavItem.d.ts +31 -0
- package/dist/molecules/NavItem/NavItem.js +72 -0
- package/dist/molecules/NavItem/context.d.ts +14 -0
- package/dist/molecules/NavItem/context.js +5 -0
- package/dist/molecules/NavItem/index.d.ts +1 -0
- package/dist/molecules/NavItem/index.js +1 -0
- package/dist/molecules/Pagination/Pagination.d.ts +32 -0
- package/dist/molecules/Pagination/Pagination.js +57 -0
- package/dist/molecules/Pagination/index.d.ts +1 -0
- package/dist/molecules/Pagination/index.js +1 -0
- package/dist/molecules/PasswordInput/PasswordInput.d.ts +14 -0
- package/dist/molecules/PasswordInput/PasswordInput.js +16 -0
- package/dist/molecules/PasswordInput/index.d.ts +1 -0
- package/dist/molecules/PasswordInput/index.js +1 -0
- package/dist/molecules/SearchField/SearchField.d.ts +24 -0
- package/dist/molecules/SearchField/SearchField.js +58 -0
- package/dist/molecules/SearchField/index.d.ts +1 -0
- package/dist/molecules/SearchField/index.js +1 -0
- package/dist/molecules/StatGroup/StatGroup.d.ts +21 -0
- package/dist/molecules/StatGroup/StatGroup.js +10 -0
- package/dist/molecules/StatGroup/index.d.ts +1 -0
- package/dist/molecules/StatGroup/index.js +1 -0
- package/dist/molecules/ThemeToggle/ThemeToggle.d.ts +23 -0
- package/dist/molecules/ThemeToggle/ThemeToggle.js +26 -0
- package/dist/molecules/ThemeToggle/index.d.ts +1 -0
- package/dist/molecules/ThemeToggle/index.js +1 -0
- package/dist/molecules/UserMenu/UserMenu.d.ts +20 -0
- package/dist/molecules/UserMenu/UserMenu.js +14 -0
- package/dist/molecules/UserMenu/index.d.ts +1 -0
- package/dist/molecules/UserMenu/index.js +1 -0
- package/dist/molecules/index.d.ts +21 -0
- package/dist/molecules/index.js +21 -0
- package/dist/organisms/AppShell/AppShell.d.ts +29 -0
- package/dist/organisms/AppShell/AppShell.js +34 -0
- package/dist/organisms/AppShell/context.d.ts +14 -0
- package/dist/organisms/AppShell/context.js +6 -0
- package/dist/organisms/AppShell/index.d.ts +1 -0
- package/dist/organisms/AppShell/index.js +1 -0
- package/dist/organisms/AuthForms/AuthForms.d.ts +83 -0
- package/dist/organisms/AuthForms/AuthForms.js +57 -0
- package/dist/organisms/AuthForms/index.d.ts +2 -0
- package/dist/organisms/AuthForms/index.js +1 -0
- package/dist/organisms/AuthForms/shared.d.ts +36 -0
- package/dist/organisms/AuthForms/shared.js +36 -0
- package/dist/organisms/BulkActionBar/BulkActionBar.d.ts +25 -0
- package/dist/organisms/BulkActionBar/BulkActionBar.js +18 -0
- package/dist/organisms/BulkActionBar/index.d.ts +1 -0
- package/dist/organisms/BulkActionBar/index.js +1 -0
- package/dist/organisms/Chart/CategoryCharts.d.ts +34 -0
- package/dist/organisms/Chart/CategoryCharts.js +93 -0
- package/dist/organisms/Chart/ChartFrame.d.ts +26 -0
- package/dist/organisms/Chart/ChartFrame.js +18 -0
- package/dist/organisms/Chart/DonutChart.d.ts +25 -0
- package/dist/organisms/Chart/DonutChart.js +53 -0
- package/dist/organisms/Chart/Sparkline.d.ts +15 -0
- package/dist/organisms/Chart/Sparkline.js +18 -0
- package/dist/organisms/Chart/index.d.ts +5 -0
- package/dist/organisms/Chart/index.js +5 -0
- package/dist/organisms/Chart/shared.d.ts +51 -0
- package/dist/organisms/Chart/shared.js +113 -0
- package/dist/organisms/DataTable/DataTable.d.ts +63 -0
- package/dist/organisms/DataTable/DataTable.js +119 -0
- package/dist/organisms/DataTable/index.d.ts +3 -0
- package/dist/organisms/DataTable/index.js +3 -0
- package/dist/organisms/DataTable/types.d.ts +39 -0
- package/dist/organisms/DataTable/types.js +1 -0
- package/dist/organisms/DataTable/useClientTable.d.ts +27 -0
- package/dist/organisms/DataTable/useClientTable.js +51 -0
- package/dist/organisms/DateRangePicker/DateRangePicker.d.ts +43 -0
- package/dist/organisms/DateRangePicker/DateRangePicker.js +54 -0
- package/dist/organisms/DateRangePicker/index.d.ts +1 -0
- package/dist/organisms/DateRangePicker/index.js +1 -0
- package/dist/organisms/FilterBar/FilterBar.d.ts +28 -0
- package/dist/organisms/FilterBar/FilterBar.js +16 -0
- package/dist/organisms/FilterBar/index.d.ts +1 -0
- package/dist/organisms/FilterBar/index.js +1 -0
- package/dist/organisms/PageHeader/PageHeader.d.ts +31 -0
- package/dist/organisms/PageHeader/PageHeader.js +19 -0
- package/dist/organisms/PageHeader/index.d.ts +1 -0
- package/dist/organisms/PageHeader/index.js +1 -0
- package/dist/organisms/PageTabs/PageTabs.d.ts +27 -0
- package/dist/organisms/PageTabs/PageTabs.js +31 -0
- package/dist/organisms/PageTabs/index.d.ts +1 -0
- package/dist/organisms/PageTabs/index.js +1 -0
- package/dist/organisms/Sidebar/Sidebar.d.ts +48 -0
- package/dist/organisms/Sidebar/Sidebar.js +61 -0
- package/dist/organisms/Sidebar/index.d.ts +1 -0
- package/dist/organisms/Sidebar/index.js +1 -0
- package/dist/organisms/Topbar/Topbar.d.ts +24 -0
- package/dist/organisms/Topbar/Topbar.js +18 -0
- package/dist/organisms/Topbar/index.d.ts +1 -0
- package/dist/organisms/Topbar/index.js +1 -0
- package/dist/organisms/index.d.ts +11 -0
- package/dist/organisms/index.js +11 -0
- package/dist/strings.d.ts +108 -15
- package/dist/strings.js +73 -7
- package/dist/templates/AdminLayout/AdminLayout.d.ts +14 -0
- package/dist/templates/AdminLayout/AdminLayout.js +9 -0
- package/dist/templates/AdminLayout/index.d.ts +1 -0
- package/dist/templates/AdminLayout/index.js +1 -0
- package/dist/templates/AuthLayout/AuthLayout.d.ts +28 -0
- package/dist/templates/AuthLayout/AuthLayout.js +11 -0
- package/dist/templates/AuthLayout/index.d.ts +1 -0
- package/dist/templates/AuthLayout/index.js +1 -0
- package/dist/templates/DashboardPage/DashboardPage.d.ts +25 -0
- package/dist/templates/DashboardPage/DashboardPage.js +12 -0
- package/dist/templates/DashboardPage/index.d.ts +1 -0
- package/dist/templates/DashboardPage/index.js +1 -0
- package/dist/templates/DetailPage/DetailPage.d.ts +20 -0
- package/dist/templates/DetailPage/DetailPage.js +11 -0
- package/dist/templates/DetailPage/index.d.ts +1 -0
- package/dist/templates/DetailPage/index.js +1 -0
- package/dist/templates/FormPage/FormPage.d.ts +26 -0
- package/dist/templates/FormPage/FormPage.js +11 -0
- package/dist/templates/FormPage/index.d.ts +1 -0
- package/dist/templates/FormPage/index.js +1 -0
- package/dist/templates/ListPage/ListPage.d.ts +17 -0
- package/dist/templates/ListPage/ListPage.js +8 -0
- package/dist/templates/ListPage/index.d.ts +1 -0
- package/dist/templates/ListPage/index.js +1 -0
- package/dist/templates/SettingsPage/SettingsPage.d.ts +32 -0
- package/dist/templates/SettingsPage/SettingsPage.js +13 -0
- package/dist/templates/SettingsPage/index.d.ts +1 -0
- package/dist/templates/SettingsPage/index.js +1 -0
- package/dist/templates/StatusPage/StatusPage.d.ts +18 -0
- package/dist/templates/StatusPage/StatusPage.js +8 -0
- package/dist/templates/StatusPage/index.d.ts +1 -0
- package/dist/templates/StatusPage/index.js +1 -0
- package/dist/templates/index.d.ts +8 -0
- package/dist/templates/index.js +8 -0
- package/dist/utils/dates.d.ts +67 -0
- package/dist/utils/dates.js +154 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/theme.d.ts +4 -0
- package/dist/utils/theme.js +7 -3
- package/package.json +4 -1
- package/tokens.css +494 -21
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// biome-ignore-all lint/a11y/noRedundantRoles: the stacked layout sets display: block on the table, which drops its implicit roles in some browsers; explicit roles keep it a table.
|
|
3
|
+
import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, DotsHorizontalIcon, } from '@radix-ui/react-icons';
|
|
4
|
+
import { IconButton, Skeleton as ThemedSkeleton } from '@radix-ui/themes';
|
|
5
|
+
import { useId } from 'react';
|
|
6
|
+
import { ActionMenu } from '../../atoms/ActionMenu/ActionMenu.js';
|
|
7
|
+
import { Checkbox } from '../../atoms/Checkbox/Checkbox.js';
|
|
8
|
+
import { EmptyState } from '../../molecules/EmptyState/EmptyState.js';
|
|
9
|
+
import { ErrorState } from '../../molecules/ErrorState/ErrorState.js';
|
|
10
|
+
import { Pagination } from '../../molecules/Pagination/Pagination.js';
|
|
11
|
+
import { useUiStrings } from '../../strings.js';
|
|
12
|
+
const INTERACTIVE = 'a, button, input, select, textarea, [role="checkbox"], [role="menuitem"]';
|
|
13
|
+
/**
|
|
14
|
+
* A data table: sortable headers, row selection, row actions, loading/empty/error states and
|
|
15
|
+
* pagination. Fully controlled: it never sorts, pages or selects on its own, so the data can
|
|
16
|
+
* come from a server as easily as from `useClientTable`.
|
|
17
|
+
*
|
|
18
|
+
* It responds to its own width (a container query), not the viewport's. Narrow, it drops
|
|
19
|
+
* low-priority columns and then stacks each row into a labelled card. Explicit table roles
|
|
20
|
+
* keep it a table to screen readers in the stacked layout, where `display: block` would
|
|
21
|
+
* otherwise erase that in some browsers.
|
|
22
|
+
*/
|
|
23
|
+
export function DataTable({ columns, rows, getRowId, caption, showCaption = false, getRowLabel, sort = null, onSortChange, selection, onSelectionChange, rowActions, onRowAction, onRowClick, state = 'ready', empty, error, loadingRows = 5, pagination, density = 'comfortable', stackBelow = 'md', stickyFirstColumn = false, maxHeight, className, }) {
|
|
24
|
+
const strings = useUiStrings();
|
|
25
|
+
const captionId = useId();
|
|
26
|
+
const selectable = !!selection && !!onSelectionChange;
|
|
27
|
+
const hasActions = !!rowActions;
|
|
28
|
+
const span = columns.length + (selectable ? 1 : 0) + (hasActions ? 1 : 0);
|
|
29
|
+
const headerColumn = columns.find((c) => c.isRowHeader);
|
|
30
|
+
const labelOf = (row) => {
|
|
31
|
+
if (getRowLabel)
|
|
32
|
+
return getRowLabel(row);
|
|
33
|
+
const v = headerColumn?.sortValue?.(row);
|
|
34
|
+
return v === null || v === undefined ? getRowId(row) : String(v);
|
|
35
|
+
};
|
|
36
|
+
const effective = state === 'ready' && rows.length === 0 ? 'empty' : state;
|
|
37
|
+
const ids = effective === 'ready' ? rows.map(getRowId) : [];
|
|
38
|
+
const selectedHere = ids.filter((id) => selection?.has(id)).length;
|
|
39
|
+
const allChecked = selectedHere === 0 ? false : selectedHere === ids.length ? true : 'indeterminate';
|
|
40
|
+
const toggleAll = () => {
|
|
41
|
+
if (!onSelectionChange)
|
|
42
|
+
return;
|
|
43
|
+
const next = new Set(selection);
|
|
44
|
+
if (allChecked === true)
|
|
45
|
+
for (const id of ids)
|
|
46
|
+
next.delete(id);
|
|
47
|
+
else
|
|
48
|
+
for (const id of ids)
|
|
49
|
+
next.add(id);
|
|
50
|
+
onSelectionChange(next);
|
|
51
|
+
};
|
|
52
|
+
const toggleOne = (id) => {
|
|
53
|
+
if (!onSelectionChange)
|
|
54
|
+
return;
|
|
55
|
+
const next = new Set(selection);
|
|
56
|
+
if (next.has(id))
|
|
57
|
+
next.delete(id);
|
|
58
|
+
else
|
|
59
|
+
next.add(id);
|
|
60
|
+
onSelectionChange(next);
|
|
61
|
+
};
|
|
62
|
+
const nextSort = (id) => {
|
|
63
|
+
if (sort?.id !== id)
|
|
64
|
+
return { id, direction: 'asc' };
|
|
65
|
+
if (sort.direction === 'asc')
|
|
66
|
+
return { id, direction: 'desc' };
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
const header = (_jsx("thead", { role: "rowgroup", children: _jsxs("tr", { role: "row", children: [selectable ? (_jsx("th", { role: "columnheader", scope: "col", "data-col": "select", children: _jsx(Checkbox, { "aria-label": strings.selectAll, checked: allChecked, disabled: ids.length === 0, onCheckedChange: toggleAll }) })) : null, columns.map((col) => {
|
|
70
|
+
const sortable = (col.sortable ?? !!col.sortValue) && !!onSortChange;
|
|
71
|
+
const sorted = sort?.id === col.id ? sort.direction : undefined;
|
|
72
|
+
const Icon = sorted === 'asc' ? ArrowUpIcon : sorted === 'desc' ? ArrowDownIcon : CaretSortIcon;
|
|
73
|
+
return (_jsx("th", { role: "columnheader", scope: "col", "data-align": col.align, "data-priority": col.priority, style: col.width ? { inlineSize: col.width } : undefined, "aria-sort": sorted === 'asc' ? 'ascending' : sorted === 'desc' ? 'descending' : undefined, children: sortable ? (_jsxs("button", { type: "button", className: "ui-table__sort", "data-sorted": sorted ? '' : undefined, onClick: () => onSortChange?.(nextSort(col.id)), children: [col.header, _jsx(Icon, { "aria-hidden": "true" })] })) : (col.header) }, col.id));
|
|
74
|
+
}), hasActions ? (_jsx("th", { role: "columnheader", scope: "col", "data-col": "actions", children: _jsx("span", { className: "ui-sr-only", children: strings.moreActions }) })) : null] }) }));
|
|
75
|
+
let body;
|
|
76
|
+
if (effective === 'loading') {
|
|
77
|
+
body = Array.from({ length: loadingRows }, (_, i) => `l${i}`).map((key) => (_jsx("tr", { role: "row", children: Array.from({ length: span }, (_, c) => `${key}c${c}`).map((ck) => (_jsx("td", { role: "cell", children: _jsx(ThemedSkeleton, { height: "0.875rem", width: "70%" }) }, ck))) }, key)));
|
|
78
|
+
}
|
|
79
|
+
else if (effective === 'error' || effective === 'empty') {
|
|
80
|
+
body = (_jsx("tr", { role: "row", children: _jsx("td", { role: "cell", colSpan: span, className: "ui-table__empty", children: effective === 'error'
|
|
81
|
+
? (error ?? _jsx(ErrorState, { size: "sm", as: "p" }))
|
|
82
|
+
: (empty ?? _jsx(EmptyState, { size: "sm", as: "p", title: strings.noResults })) }) }));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
body = rows.map((row) => {
|
|
86
|
+
const id = getRowId(row);
|
|
87
|
+
const label = labelOf(row);
|
|
88
|
+
const selected = selection?.has(id) ?? false;
|
|
89
|
+
const onClick = onRowClick
|
|
90
|
+
? (e) => {
|
|
91
|
+
const hit = e.target.closest(INTERACTIVE);
|
|
92
|
+
if (hit && e.currentTarget.contains(hit))
|
|
93
|
+
return;
|
|
94
|
+
onRowClick(row);
|
|
95
|
+
}
|
|
96
|
+
: undefined;
|
|
97
|
+
return (_jsxs("tr", { role: "row", "data-selected": selected ? '' : undefined, "data-clickable": onRowClick ? '' : undefined, onClick: onClick, children: [selectable ? (_jsx("td", { role: "cell", "data-col": "select", children: _jsx(Checkbox, { "aria-label": strings.selectRow(label), checked: selected, onCheckedChange: () => toggleOne(id) }) })) : null, columns.map((col) => {
|
|
98
|
+
const content = col.cell(row);
|
|
99
|
+
const common = {
|
|
100
|
+
'data-align': col.align,
|
|
101
|
+
'data-priority': col.priority,
|
|
102
|
+
'data-label': col.mobileLabel === false || col.isRowHeader
|
|
103
|
+
? undefined
|
|
104
|
+
: (col.mobileLabel ?? col.header),
|
|
105
|
+
};
|
|
106
|
+
if (col.isRowHeader) {
|
|
107
|
+
return (_jsx("th", { role: "rowheader", scope: "row", ...common, children: onRowClick ? (_jsx("button", { type: "button", className: "ui-table__rowbutton", onClick: () => onRowClick(row), children: content })) : (content) }, col.id));
|
|
108
|
+
}
|
|
109
|
+
return (_jsx("td", { role: "cell", ...common, children: content }, col.id));
|
|
110
|
+
}), hasActions ? (_jsx("td", { role: "cell", "data-col": "actions", children: _jsx(ActionMenu, { items: rowActions(row), onSelect: (actionId) => onRowAction?.(actionId, row), title: label, trigger: _jsx(IconButton, { variant: "ghost", color: "gray", "aria-label": strings.actionsFor(label), children: _jsx(DotsHorizontalIcon, {}) }) }) })) : null] }, id));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const regionStyle = maxHeight
|
|
114
|
+
? { maxBlockSize: maxHeight }
|
|
115
|
+
: undefined;
|
|
116
|
+
return (_jsxs("div", { className: ['ui-table', className].filter(Boolean).join(' '), "data-density": density, "data-stack": stackBelow || undefined, "data-sticky-first": stickyFirstColumn ? '' : undefined, children: [effective === 'loading' ? (_jsx("span", { role: "status", className: "ui-sr-only", children: strings.loading })) : null, _jsx("section", { className: "ui-table__region", "aria-labelledby": captionId,
|
|
117
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: a scrolling region must be reachable by keyboard to be scrolled.
|
|
118
|
+
tabIndex: 0, style: regionStyle, children: _jsxs("table", { className: "ui-table__table", role: "table", "aria-busy": effective === 'loading' || undefined, children: [_jsx("caption", { id: captionId, className: showCaption ? 'ui-table__caption' : 'ui-sr-only', children: caption }), header, _jsx("tbody", { role: "rowgroup", children: body })] }) }), pagination && effective !== 'loading' ? _jsx(Pagination, { ...pagination }) : null] }));
|
|
119
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type SortDirection = 'asc' | 'desc';
|
|
3
|
+
export type SortState = {
|
|
4
|
+
id: string;
|
|
5
|
+
direction: SortDirection;
|
|
6
|
+
} | null;
|
|
7
|
+
/** What a column sorts by. `null` and `undefined` sort last in both directions. */
|
|
8
|
+
export type SortValue = string | number | boolean | Date | null | undefined;
|
|
9
|
+
export interface DataTableColumn<Row> {
|
|
10
|
+
/** Stable, and what `sort.id` refers to. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** The column's name: the header text, the sort button's name, and the stacked-layout label. */
|
|
13
|
+
header: string;
|
|
14
|
+
cell: (row: Row) => ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* What the column sorts by. Needed by `useClientTable`; a server-sorted table can leave it
|
|
17
|
+
* out and set `sortable` alone.
|
|
18
|
+
*/
|
|
19
|
+
sortValue?: (row: Row) => SortValue;
|
|
20
|
+
/** Shows a sort button in the header. Default: `true` when `sortValue` is given. */
|
|
21
|
+
sortable?: boolean;
|
|
22
|
+
/** `end` also turns on tabular numerals, for numbers that should line up. */
|
|
23
|
+
align?: 'start' | 'end' | 'center';
|
|
24
|
+
/** A CSS width for the column, e.g. `'120px'` or `'20%'`. */
|
|
25
|
+
width?: string;
|
|
26
|
+
/**
|
|
27
|
+
* What goes first when the table is narrow: `low` columns hide below 640px of table width,
|
|
28
|
+
* `medium` below 480px. `high` (the default) never hides.
|
|
29
|
+
*/
|
|
30
|
+
priority?: 'high' | 'medium' | 'low';
|
|
31
|
+
/**
|
|
32
|
+
* The cell that names the row (`th scope="row"`): the card title in the stacked layout, the
|
|
33
|
+
* sticky column with `stickyFirstColumn`, and the default row label for checkboxes and menus.
|
|
34
|
+
* One column at most.
|
|
35
|
+
*/
|
|
36
|
+
isRowHeader?: boolean;
|
|
37
|
+
/** The label beside the value in the stacked layout. Default `header`; `false` for none. */
|
|
38
|
+
mobileLabel?: string | false;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DataTableColumn, SortState } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Rows sorted by one column, without mutating the input. Strings compare with `Intl.Collator`
|
|
4
|
+
* in `locale` with numeric collation ("Item 2" before "Item 10"); empty values go last in both
|
|
5
|
+
* directions; ties keep their original order.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sortRows<Row>(rows: readonly Row[], columns: readonly DataTableColumn<Row>[], sort: SortState, locale?: string): Row[];
|
|
8
|
+
export interface ClientTableOptions {
|
|
9
|
+
sort: SortState;
|
|
10
|
+
/** 1-based. */
|
|
11
|
+
page: number;
|
|
12
|
+
pageSize: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ClientTable<Row> {
|
|
15
|
+
/** The current page's rows, sorted. */
|
|
16
|
+
rows: Row[];
|
|
17
|
+
/** Every row, sorted (for "select all across pages", exports). */
|
|
18
|
+
sorted: Row[];
|
|
19
|
+
total: number;
|
|
20
|
+
pageCount: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Sorting and paging in the browser, for data that is already all loaded. A server-driven table
|
|
24
|
+
* does neither and passes its page straight to `DataTable`. Formats with the `UiStringsProvider`
|
|
25
|
+
* locale.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useClientTable<Row>(rows: readonly Row[], columns: readonly DataTableColumn<Row>[], { sort, page, pageSize }: ClientTableOptions): ClientTable<Row>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useUiLocale } from '../../strings.js';
|
|
3
|
+
function rank(v) {
|
|
4
|
+
if (v === null || v === undefined)
|
|
5
|
+
return null;
|
|
6
|
+
if (v instanceof Date)
|
|
7
|
+
return v.getTime();
|
|
8
|
+
if (typeof v === 'boolean')
|
|
9
|
+
return v ? 1 : 0;
|
|
10
|
+
return v;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Rows sorted by one column, without mutating the input. Strings compare with `Intl.Collator`
|
|
14
|
+
* in `locale` with numeric collation ("Item 2" before "Item 10"); empty values go last in both
|
|
15
|
+
* directions; ties keep their original order.
|
|
16
|
+
*/
|
|
17
|
+
export function sortRows(rows, columns, sort, locale) {
|
|
18
|
+
const column = sort ? columns.find((c) => c.id === sort.id) : undefined;
|
|
19
|
+
if (!sort || !column?.sortValue)
|
|
20
|
+
return [...rows];
|
|
21
|
+
const get = column.sortValue;
|
|
22
|
+
const collator = new Intl.Collator(locale, { numeric: true, sensitivity: 'base' });
|
|
23
|
+
const sign = sort.direction === 'asc' ? 1 : -1;
|
|
24
|
+
return rows
|
|
25
|
+
.map((row, index) => ({ row, index, value: rank(get(row)) }))
|
|
26
|
+
.sort((a, b) => {
|
|
27
|
+
if (a.value === null || b.value === null) {
|
|
28
|
+
if (a.value === b.value)
|
|
29
|
+
return a.index - b.index;
|
|
30
|
+
return a.value === null ? 1 : -1;
|
|
31
|
+
}
|
|
32
|
+
const diff = typeof a.value === 'string' || typeof b.value === 'string'
|
|
33
|
+
? collator.compare(String(a.value), String(b.value))
|
|
34
|
+
: a.value - b.value;
|
|
35
|
+
return diff === 0 ? a.index - b.index : diff * sign;
|
|
36
|
+
})
|
|
37
|
+
.map((x) => x.row);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Sorting and paging in the browser, for data that is already all loaded. A server-driven table
|
|
41
|
+
* does neither and passes its page straight to `DataTable`. Formats with the `UiStringsProvider`
|
|
42
|
+
* locale.
|
|
43
|
+
*/
|
|
44
|
+
export function useClientTable(rows, columns, { sort, page, pageSize }) {
|
|
45
|
+
const locale = useUiLocale();
|
|
46
|
+
const sorted = useMemo(() => sortRows(rows, columns, sort, locale), [rows, columns, sort, locale]);
|
|
47
|
+
const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize));
|
|
48
|
+
const current = Math.min(Math.max(page, 1), pageCount);
|
|
49
|
+
const pageRows = useMemo(() => sorted.slice((current - 1) * pageSize, current * pageSize), [sorted, current, pageSize]);
|
|
50
|
+
return { rows: pageRows, sorted, total: sorted.length, pageCount };
|
|
51
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Breakpoint } from '../../hooks/breakpoints.js';
|
|
2
|
+
import { type IsoDate, type Weekday } from '../../utils/dates.js';
|
|
3
|
+
export interface DateRangePreset {
|
|
4
|
+
id: string;
|
|
5
|
+
/** "Last 7 days", "This month". */
|
|
6
|
+
label: string;
|
|
7
|
+
range: {
|
|
8
|
+
from: IsoDate;
|
|
9
|
+
to: IsoDate;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface DateRangePickerProps {
|
|
13
|
+
/** A complete range, or `null`. A half-chosen range never leaves the picker. */
|
|
14
|
+
value: {
|
|
15
|
+
from: IsoDate;
|
|
16
|
+
to: IsoDate;
|
|
17
|
+
} | null;
|
|
18
|
+
onValueChange: (value: {
|
|
19
|
+
from: IsoDate;
|
|
20
|
+
to: IsoDate;
|
|
21
|
+
}) => void;
|
|
22
|
+
/** Ready-made ranges, chosen in one click. */
|
|
23
|
+
presets?: DateRangePreset[];
|
|
24
|
+
min?: IsoDate;
|
|
25
|
+
max?: IsoDate;
|
|
26
|
+
isDateDisabled?: (date: IsoDate) => boolean;
|
|
27
|
+
locale?: string;
|
|
28
|
+
weekStartsOn?: Weekday;
|
|
29
|
+
/** Below this it opens in a bottom Sheet with one month. Default `sm`. `false` never does. */
|
|
30
|
+
sheetBelow?: Breakpoint | false;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
id?: string;
|
|
33
|
+
'aria-describedby'?: string;
|
|
34
|
+
'aria-label'?: string;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A from–to date range: a button showing the range that opens two months side by side with
|
|
39
|
+
* presets beside them, or, below `sheetBelow`, one month in a bottom sheet with the presets as
|
|
40
|
+
* chips. Choosing the second date, or a preset, commits and closes. Escape or a click outside
|
|
41
|
+
* leaves the value as it was.
|
|
42
|
+
*/
|
|
43
|
+
export declare function DateRangePicker({ value, onValueChange, presets, min, max, isDateDisabled, locale: localeProp, weekStartsOn, sheetBelow, placeholder, id, 'aria-describedby': describedBy, 'aria-label': ariaLabel, className, }: DateRangePickerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { CalendarIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { Button as ThemedButton, Popover as ThemedPopover } from '@radix-ui/themes';
|
|
4
|
+
import { useId, useState } from 'react';
|
|
5
|
+
import { ChipGroup } from '../../atoms/ChipGroup/ChipGroup.js';
|
|
6
|
+
import { Sheet } from '../../atoms/Sheet/Sheet.js';
|
|
7
|
+
import { minWidth } from '../../hooks/breakpoints.js';
|
|
8
|
+
import { useMediaQuery } from '../../hooks/useMediaQuery.js';
|
|
9
|
+
import { Calendar } from '../../molecules/Calendar/Calendar.js';
|
|
10
|
+
import { useUiLocale, useUiStrings } from '../../strings.js';
|
|
11
|
+
import { formatDateRange } from '../../utils/dates.js';
|
|
12
|
+
/**
|
|
13
|
+
* A from–to date range: a button showing the range that opens two months side by side with
|
|
14
|
+
* presets beside them, or, below `sheetBelow`, one month in a bottom sheet with the presets as
|
|
15
|
+
* chips. Choosing the second date, or a preset, commits and closes. Escape or a click outside
|
|
16
|
+
* leaves the value as it was.
|
|
17
|
+
*/
|
|
18
|
+
export function DateRangePicker({ value, onValueChange, presets, min, max, isDateDisabled, locale: localeProp, weekStartsOn, sheetBelow = 'sm', placeholder, id, 'aria-describedby': describedBy, 'aria-label': ariaLabel, className, }) {
|
|
19
|
+
const strings = useUiStrings();
|
|
20
|
+
const providerLocale = useUiLocale();
|
|
21
|
+
const locale = localeProp ?? providerLocale;
|
|
22
|
+
const wide = useMediaQuery(minWidth(sheetBelow || 'xs'));
|
|
23
|
+
const sheet = sheetBelow !== false && !wide;
|
|
24
|
+
const [open, setOpenState] = useState(false);
|
|
25
|
+
const [draft, setDraft] = useState(value);
|
|
26
|
+
const valueId = useId();
|
|
27
|
+
const setOpen = (next) => {
|
|
28
|
+
if (next)
|
|
29
|
+
setDraft(value);
|
|
30
|
+
setOpenState(next);
|
|
31
|
+
};
|
|
32
|
+
const commit = (range) => {
|
|
33
|
+
onValueChange(range);
|
|
34
|
+
setOpenState(false);
|
|
35
|
+
};
|
|
36
|
+
const text = value ? formatDateRange(value, locale) : (placeholder ?? strings.chooseDates);
|
|
37
|
+
const title = ariaLabel ?? strings.chooseDates;
|
|
38
|
+
const trigger = (_jsxs(ThemedButton, { type: "button", variant: "surface", color: "gray", id: id, "aria-haspopup": "dialog", "aria-label": ariaLabel, "aria-describedby": [value ? valueId : null, describedBy].filter(Boolean).join(' ') || undefined, className: ['ui-date-input', className].filter(Boolean).join(' '), "data-empty": value ? undefined : '', onClick: sheet ? () => setOpen(true) : undefined, children: [_jsx(CalendarIcon, { "aria-hidden": "true" }), _jsx("span", { "aria-hidden": value ? 'true' : undefined, children: text }), value ? (_jsx("span", { id: valueId, className: "ui-sr-only", children: formatDateRange(value, locale, { dateStyle: 'full' }) })) : null] }));
|
|
39
|
+
const calendar = (_jsx(Calendar, { mode: "range", value: draft, onValueChange: (next) => {
|
|
40
|
+
setDraft(next);
|
|
41
|
+
if (next.to)
|
|
42
|
+
commit({ from: next.from, to: next.to });
|
|
43
|
+
}, min: min, max: max, isDateDisabled: isDateDisabled, locale: locale, weekStartsOn: weekStartsOn, numberOfMonths: sheet ? 1 : 2, autoFocus: true }));
|
|
44
|
+
const presetValue = presets?.find((p) => value && p.range.from === value.from && p.range.to === value.to)?.id ??
|
|
45
|
+
null;
|
|
46
|
+
if (sheet) {
|
|
47
|
+
return (_jsxs(_Fragment, { children: [trigger, _jsx(Sheet, { open: open, onOpenChange: setOpen, title: title, showCloseButton: true, children: _jsxs("div", { className: "flex flex-col gap-3", children: [presets?.length ? (_jsx(ChipGroup, { "aria-label": strings.datePresets, options: presets.map((p) => ({ value: p.id, label: p.label })), value: presetValue, onValueChange: (pid) => {
|
|
48
|
+
const preset = presets.find((p) => p.id === pid);
|
|
49
|
+
if (preset)
|
|
50
|
+
commit(preset.range);
|
|
51
|
+
} })) : null, calendar] }) })] }));
|
|
52
|
+
}
|
|
53
|
+
return (_jsxs(ThemedPopover.Root, { open: open, onOpenChange: setOpen, children: [_jsx(ThemedPopover.Trigger, { children: trigger }), _jsx(ThemedPopover.Content, { align: "start", "aria-label": title, className: "max-w-[calc(100vw-1.5rem)]", children: _jsxs("div", { className: "ui-range-picker", children: [presets?.length ? (_jsx("ul", { className: "ui-range-picker__presets", "aria-label": strings.datePresets, children: presets.map((p) => (_jsx("li", { children: _jsx("button", { type: "button", className: "ui-nav-item", "aria-pressed": p.id === presetValue, onClick: () => commit(p.range), children: _jsx("span", { className: "ui-nav-item__label", children: p.label }) }) }, p.id))) })) : null, calendar] }) })] }));
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './DateRangePicker.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './DateRangePicker.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface ActiveFilter {
|
|
3
|
+
id: string;
|
|
4
|
+
/** What the pill says, e.g. "Status: Paid". Also names its remove button. */
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FilterBarProps {
|
|
8
|
+
/** A SearchField. Takes the remaining width, and a row of its own below 640px. */
|
|
9
|
+
search?: ReactNode;
|
|
10
|
+
/** FilterPopovers, Selects, FilterChips. */
|
|
11
|
+
filters?: ReactNode;
|
|
12
|
+
/** At the end of the row: an export button, a view switch. */
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
/** The filters in effect, shown as removable pills under the row. */
|
|
15
|
+
activeFilters?: ActiveFilter[];
|
|
16
|
+
onRemoveFilter?: (id: string) => void;
|
|
17
|
+
/** Shows "Clear all" after the pills. */
|
|
18
|
+
onClearAll?: () => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The controls above a list: search, filters, actions, and the filters in effect.
|
|
23
|
+
*
|
|
24
|
+
* It lays out by its own width. On a phone the search takes the first row and everything else
|
|
25
|
+
* wraps below it. Put the filter controls in a FilterPopover and they become one "Filters (n)"
|
|
26
|
+
* button that opens a sheet.
|
|
27
|
+
*/
|
|
28
|
+
export declare function FilterBar({ search, filters, actions, activeFilters, onRemoveFilter, onClearAll, className, }: FilterBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '../../atoms/Button/Button.js';
|
|
3
|
+
import { Pill } from '../../atoms/Pill/Pill.js';
|
|
4
|
+
import { useUiStrings } from '../../strings.js';
|
|
5
|
+
/**
|
|
6
|
+
* The controls above a list: search, filters, actions, and the filters in effect.
|
|
7
|
+
*
|
|
8
|
+
* It lays out by its own width. On a phone the search takes the first row and everything else
|
|
9
|
+
* wraps below it. Put the filter controls in a FilterPopover and they become one "Filters (n)"
|
|
10
|
+
* button that opens a sheet.
|
|
11
|
+
*/
|
|
12
|
+
export function FilterBar({ search, filters, actions, activeFilters, onRemoveFilter, onClearAll, className, }) {
|
|
13
|
+
const strings = useUiStrings();
|
|
14
|
+
const hasActive = !!activeFilters?.length;
|
|
15
|
+
return (_jsxs("div", { className: ['ui-filter-bar', className].filter(Boolean).join(' '), children: [_jsxs("div", { className: "ui-filter-bar__row", children: [search ? _jsx("div", { className: "ui-filter-bar__search", children: search }) : null, filters, actions ? _jsx("div", { className: "ui-filter-bar__actions", children: actions }) : null] }), hasActive ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("ul", { className: "ui-filter-bar__active", "aria-label": strings.activeFilters, children: activeFilters?.map((filter) => (_jsx("li", { children: _jsx(Pill, { onRemove: onRemoveFilter ? () => onRemoveFilter(filter.id) : undefined, children: filter.label }) }, filter.id))) }), onClearAll ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: onClearAll, children: strings.clearFilters })) : null] })) : null] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FilterBar.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FilterBar.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type ActionMenuItem } from '../../atoms/ActionMenu/ActionMenu.js';
|
|
3
|
+
import { type BreadcrumbItem } from '../../molecules/Breadcrumbs/Breadcrumbs.js';
|
|
4
|
+
export interface PageHeaderProps {
|
|
5
|
+
/** The page's `h1`. */
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
/** A trail (rendered as Breadcrumbs), or any node of your own. */
|
|
9
|
+
breadcrumbs?: BreadcrumbItem[] | ReactNode;
|
|
10
|
+
/** The one main action. Always visible. */
|
|
11
|
+
primaryAction?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Lesser actions. Ghost buttons beside the primary one when the header is 640px or wider;
|
|
14
|
+
* folded into a "more" menu when it is narrower.
|
|
15
|
+
*/
|
|
16
|
+
secondaryActions?: ActionMenuItem[];
|
|
17
|
+
onSecondaryAction?: (id: string) => void;
|
|
18
|
+
/** Beside the title: a status badge, a last-updated time. */
|
|
19
|
+
meta?: ReactNode;
|
|
20
|
+
/** Under everything: PageTabs, usually. */
|
|
21
|
+
tabs?: ReactNode;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The top of a page: breadcrumbs, the `h1`, a description and the page's actions.
|
|
26
|
+
*
|
|
27
|
+
* It sizes to its own column, not the viewport (a container query), so it behaves the same in
|
|
28
|
+
* a narrow panel as on a phone. Both forms of the secondary actions are in the markup; the one
|
|
29
|
+
* that does not apply is `display: none`, which also takes it out of the accessibility tree.
|
|
30
|
+
*/
|
|
31
|
+
export declare function PageHeader({ title, description, breadcrumbs, primaryAction, secondaryActions, onSecondaryAction, meta, tabs, className, }: PageHeaderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { DotsHorizontalIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { IconButton } from '@radix-ui/themes';
|
|
4
|
+
import { ActionMenu } from '../../atoms/ActionMenu/ActionMenu.js';
|
|
5
|
+
import { Button } from '../../atoms/Button/Button.js';
|
|
6
|
+
import { Breadcrumbs } from '../../molecules/Breadcrumbs/Breadcrumbs.js';
|
|
7
|
+
import { useUiStrings } from '../../strings.js';
|
|
8
|
+
/**
|
|
9
|
+
* The top of a page: breadcrumbs, the `h1`, a description and the page's actions.
|
|
10
|
+
*
|
|
11
|
+
* It sizes to its own column, not the viewport (a container query), so it behaves the same in
|
|
12
|
+
* a narrow panel as on a phone. Both forms of the secondary actions are in the markup; the one
|
|
13
|
+
* that does not apply is `display: none`, which also takes it out of the accessibility tree.
|
|
14
|
+
*/
|
|
15
|
+
export function PageHeader({ title, description, breadcrumbs, primaryAction, secondaryActions, onSecondaryAction, meta, tabs, className, }) {
|
|
16
|
+
const strings = useUiStrings();
|
|
17
|
+
const hasSecondary = !!secondaryActions?.length && !!onSecondaryAction;
|
|
18
|
+
return (_jsxs("div", { className: ['ui-page-header', className].filter(Boolean).join(' '), children: [Array.isArray(breadcrumbs) ? _jsx(Breadcrumbs, { items: breadcrumbs }) : breadcrumbs, _jsxs("div", { className: "ui-page-header__row", children: [_jsxs("div", { className: "ui-page-header__text", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("h1", { className: "ui-page-header__title", children: title }), meta] }), description ? _jsx("p", { className: "ui-page-header__description", children: description }) : null] }), primaryAction || hasSecondary ? (_jsxs("div", { className: "ui-page-header__actions", children: [hasSecondary ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "ui-page-header__secondary", children: secondaryActions?.map((a) => (_jsx(Button, { variant: a.destructive ? 'danger' : 'ghost', disabled: a.disabled, onClick: () => onSecondaryAction?.(a.id), children: a.label }, a.id))) }), _jsx("span", { className: "ui-page-header__overflow", children: _jsx(ActionMenu, { items: secondaryActions ?? [], onSelect: (id) => onSecondaryAction?.(id), trigger: _jsx(IconButton, { variant: "ghost", color: "gray", "aria-label": strings.moreActions, children: _jsx(DotsHorizontalIcon, {}) }) }) })] })) : null, primaryAction] })) : null] }), tabs] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PageHeader.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PageHeader.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RenderLink } from '../../atoms/TextLink/TextLink.js';
|
|
2
|
+
export interface PageTabsItem {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** A link. Without it the tab is a button that calls `onValueChange`. */
|
|
6
|
+
href?: string;
|
|
7
|
+
render?: RenderLink;
|
|
8
|
+
/** A count after the label, e.g. open items. */
|
|
9
|
+
count?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface PageTabsProps {
|
|
12
|
+
items: PageTabsItem[];
|
|
13
|
+
value: string;
|
|
14
|
+
onValueChange?: (value: string) => void;
|
|
15
|
+
/** Names the navigation landmark. Required: a page can have more than one. */
|
|
16
|
+
'aria-label': string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sub-pages of one page (Overview, Activity, Settings), shown as a row of tabs.
|
|
21
|
+
*
|
|
22
|
+
* Navigation rather than an ARIA tablist: each tab goes somewhere (usually a URL), so it is a
|
|
23
|
+
* `nav` of links with `aria-current="page"`, the pattern people expect from the back button
|
|
24
|
+
* and from opening a tab in a new window. For tabs that switch panels in place, use the `Tabs`
|
|
25
|
+
* primitive instead. The row scrolls sideways when the tabs do not fit.
|
|
26
|
+
*/
|
|
27
|
+
export declare function PageTabs({ items, value, onValueChange, 'aria-label': ariaLabel, className, }: PageTabsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Sub-pages of one page (Overview, Activity, Settings), shown as a row of tabs.
|
|
4
|
+
*
|
|
5
|
+
* Navigation rather than an ARIA tablist: each tab goes somewhere (usually a URL), so it is a
|
|
6
|
+
* `nav` of links with `aria-current="page"`, the pattern people expect from the back button
|
|
7
|
+
* and from opening a tab in a new window. For tabs that switch panels in place, use the `Tabs`
|
|
8
|
+
* primitive instead. The row scrolls sideways when the tabs do not fit.
|
|
9
|
+
*/
|
|
10
|
+
export function PageTabs({ items, value, onValueChange, 'aria-label': ariaLabel, className, }) {
|
|
11
|
+
return (_jsx("nav", { "aria-label": ariaLabel, className: className, children: _jsx("ul", { className: "ui-page-tabs__list", children: items.map((item) => {
|
|
12
|
+
const active = item.value === value;
|
|
13
|
+
const children = (_jsxs(_Fragment, { children: [item.label, item.count !== undefined ? ' ' : null, item.count !== undefined ? (_jsx("span", { className: "text-xs tabular-nums text-ink-muted", children: item.count })) : null] }));
|
|
14
|
+
const onClick = () => onValueChange?.(item.value);
|
|
15
|
+
let control;
|
|
16
|
+
if (item.href !== undefined) {
|
|
17
|
+
const props = {
|
|
18
|
+
href: item.href,
|
|
19
|
+
className: 'ui-page-tabs__item',
|
|
20
|
+
'aria-current': active ? 'page' : undefined,
|
|
21
|
+
onClick,
|
|
22
|
+
children,
|
|
23
|
+
};
|
|
24
|
+
control = item.render ? item.render(props) : _jsx("a", { ...props });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
control = (_jsx("button", { type: "button", className: "ui-page-tabs__item", "aria-current": active ? 'page' : undefined, onClick: onClick, children: children }));
|
|
28
|
+
}
|
|
29
|
+
return _jsx("li", { children: control }, item.value);
|
|
30
|
+
}) }) }));
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PageTabs.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PageTabs.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type Breakpoint } from '../../hooks/breakpoints.js';
|
|
3
|
+
import { type NavItemProps } from '../../molecules/NavItem/NavItem.js';
|
|
4
|
+
export interface SidebarSection {
|
|
5
|
+
/** React key. Defaults to the label, then the index. */
|
|
6
|
+
id?: string;
|
|
7
|
+
/** A small heading over the section. Hidden (still read out) while collapsed. */
|
|
8
|
+
label?: string;
|
|
9
|
+
items: NavItemProps[];
|
|
10
|
+
}
|
|
11
|
+
export interface SidebarProps {
|
|
12
|
+
/** Top slot: a logo or workspace switcher. */
|
|
13
|
+
header?: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* What the header shows while the sidebar is an icon rail: a logo mark rather than the full
|
|
16
|
+
* wordmark, which would wrap in 64px. Default: `header`.
|
|
17
|
+
*/
|
|
18
|
+
collapsedHeader?: ReactNode;
|
|
19
|
+
sections: SidebarSection[];
|
|
20
|
+
/** Bottom slot, above the collapse toggle: help links, a user menu. */
|
|
21
|
+
footer?: ReactNode;
|
|
22
|
+
/** Controlled icon-rail state on wide screens. */
|
|
23
|
+
collapsed?: boolean;
|
|
24
|
+
defaultCollapsed?: boolean;
|
|
25
|
+
onCollapsedChange?: (collapsed: boolean) => void;
|
|
26
|
+
/** Shows the collapse toggle. Default `true`. */
|
|
27
|
+
collapsible?: boolean;
|
|
28
|
+
/** Overrides the landmark's name (otherwise the `mainNavigation` UI string). */
|
|
29
|
+
'aria-label'?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Outside an AppShell: the breakpoint from which the sidebar is persistent (default `md`), and
|
|
32
|
+
* the off-canvas panel's open state below it. Inside one, the shell provides all three.
|
|
33
|
+
*/
|
|
34
|
+
breakpoint?: Breakpoint;
|
|
35
|
+
open?: boolean;
|
|
36
|
+
onOpenChange?: (open: boolean) => void;
|
|
37
|
+
/** Where the off-canvas panel portals. Default: the nearest `<Theme>`. */
|
|
38
|
+
container?: HTMLElement | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The main navigation. From `breakpoint` up it is a persistent column that can collapse to an
|
|
42
|
+
* icon rail; below it, an off-canvas panel from the start edge with a scrim, a focus trap and
|
|
43
|
+
* Escape, which closes again when an item is chosen.
|
|
44
|
+
*
|
|
45
|
+
* Inside an `AppShell`, the shell decides which and the Topbar's menu button opens the panel.
|
|
46
|
+
* Without matchMedia (on the server) it renders the persistent column.
|
|
47
|
+
*/
|
|
48
|
+
export declare function Sidebar({ header, collapsedHeader, sections, footer, collapsed: collapsedProp, defaultCollapsed, onCollapsedChange, collapsible, 'aria-label': ariaLabel, breakpoint, open, onOpenChange, container, }: SidebarProps): import("react").JSX.Element;
|