@kennycorrea/ui 0.11.3 → 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 +189 -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/Card/Card.d.ts +5 -3
- package/dist/atoms/Card/Card.js +21 -4
- package/dist/atoms/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/atoms/ContextMenu/ContextMenu.js +1 -1
- package/dist/atoms/Drawer/Drawer.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/Sheet/Sheet.js +1 -1
- 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 +513 -21
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import { Label } from '../../atoms/Label/Label.js';
|
|
5
|
+
import { Select } from '../../atoms/Select/Select.js';
|
|
6
|
+
import { useUiStrings } from '../../strings.js';
|
|
7
|
+
/**
|
|
8
|
+
* The page numbers to show: always the first and last, `siblings` either side of the current
|
|
9
|
+
* one, and a gap marker wherever pages are skipped. A gap of exactly one page shows that page
|
|
10
|
+
* instead, since "…" would take the same room and say less; for the same reason a short run
|
|
11
|
+
* (up to `2 * siblings + 5` pages) shows every page.
|
|
12
|
+
*/
|
|
13
|
+
export function paginationRange(page, pageCount, siblings = 1) {
|
|
14
|
+
if (pageCount <= 0)
|
|
15
|
+
return [];
|
|
16
|
+
// When every page fits in the slots a gapped range would take anyway, show them all.
|
|
17
|
+
if (pageCount <= 2 * siblings + 5)
|
|
18
|
+
return Array.from({ length: pageCount }, (_, i) => i + 1);
|
|
19
|
+
const current = Math.min(Math.max(page, 1), pageCount);
|
|
20
|
+
const start = Math.max(2, current - siblings);
|
|
21
|
+
const end = Math.min(pageCount - 1, current + siblings);
|
|
22
|
+
const out = [1];
|
|
23
|
+
if (start === 3)
|
|
24
|
+
out.push(2);
|
|
25
|
+
else if (start > 3)
|
|
26
|
+
out.push('gap-start');
|
|
27
|
+
for (let p = start; p <= end; p++)
|
|
28
|
+
out.push(p);
|
|
29
|
+
if (end === pageCount - 2)
|
|
30
|
+
out.push(pageCount - 1);
|
|
31
|
+
else if (end < pageCount - 2)
|
|
32
|
+
out.push('gap-end');
|
|
33
|
+
if (pageCount > 1)
|
|
34
|
+
out.push(pageCount);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Page navigation for a list or table, controlled: it never changes page by itself.
|
|
39
|
+
*
|
|
40
|
+
* A `nav` landmark. At 520px of its own width or more it shows numbered pages; narrower, just
|
|
41
|
+
* previous / "Page 2 of 10" / next, and the rows-per-page select is dropped. Both are in the
|
|
42
|
+
* markup and CSS shows one, so nothing re-renders on resize.
|
|
43
|
+
*/
|
|
44
|
+
export function Pagination({ page, onPageChange, pageCount: pageCountProp, total, pageSize, pageSizeOptions, onPageSizeChange, siblings = 1, className, }) {
|
|
45
|
+
const strings = useUiStrings();
|
|
46
|
+
const sizeId = useId();
|
|
47
|
+
const pageCount = Math.max(1, pageCountProp ?? (total !== undefined && pageSize ? Math.ceil(total / pageSize) : 1));
|
|
48
|
+
const current = Math.min(Math.max(page, 1), pageCount);
|
|
49
|
+
const go = (p) => {
|
|
50
|
+
if (p !== current && p >= 1 && p <= pageCount)
|
|
51
|
+
onPageChange(p);
|
|
52
|
+
};
|
|
53
|
+
const range = total !== undefined && pageSize && total > 0
|
|
54
|
+
? strings.rangeOf((current - 1) * pageSize + 1, Math.min(current * pageSize, total), total)
|
|
55
|
+
: null;
|
|
56
|
+
return (_jsx("nav", { "aria-label": strings.pagination, className: ['ui-pagination', className].filter(Boolean).join(' '), children: _jsxs("div", { className: "ui-pagination__bar", children: [range ? _jsx("span", { className: "tabular-nums", children: range }) : _jsx("span", {}), pageSizeOptions && onPageSizeChange && pageSize ? (_jsxs("span", { className: "ui-pagination__size", children: [_jsx(Label, { htmlFor: sizeId, children: strings.rowsPerPage }), _jsx(Select, { id: sizeId, value: String(pageSize), onValueChange: (v) => onPageSizeChange(Number(v)), options: pageSizeOptions.map((n) => ({ value: String(n), label: String(n) })) })] })) : null, _jsxs("span", { className: "ui-pagination__nav", children: [_jsx("button", { type: "button", className: "ui-pagination__page", "aria-label": strings.previousPage, disabled: current <= 1, onClick: () => go(current - 1), children: _jsx(ChevronLeftIcon, { "aria-hidden": "true" }) }), _jsx("ul", { className: "ui-pagination__pages", children: paginationRange(current, pageCount, siblings).map((entry) => typeof entry === 'number' ? (_jsx("li", { children: _jsx("button", { type: "button", className: "ui-pagination__page", "aria-label": strings.goToPage(entry), "aria-current": entry === current ? 'page' : undefined, onClick: () => go(entry), children: entry }) }, entry)) : (_jsx("li", { className: "ui-pagination__ellipsis", "aria-hidden": "true", children: "\u2026" }, entry))) }), _jsx("span", { className: "ui-pagination__compact", children: strings.pageOf(current, pageCount) }), _jsx("button", { type: "button", className: "ui-pagination__page", "aria-label": strings.nextPage, disabled: current >= pageCount, onClick: () => go(current + 1), children: _jsx(ChevronRightIcon, { "aria-hidden": "true" }) })] })] }) }));
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Pagination.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Pagination.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type InputProps } from '../../atoms/Input/Input.js';
|
|
2
|
+
export interface PasswordInputProps extends Omit<InputProps, 'type' | 'autoComplete'> {
|
|
3
|
+
/**
|
|
4
|
+
* `current-password` to sign in, `new-password` to create one. Required, because password
|
|
5
|
+
* managers rely on it to fill the right one and to offer a strong new one.
|
|
6
|
+
*/
|
|
7
|
+
autoComplete: 'current-password' | 'new-password';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A password field with a show/hide toggle. The toggle is a pressed/unpressed button with one
|
|
11
|
+
* name (the `showPassword` UI string). Showing turns off spellcheck and autocapitalisation
|
|
12
|
+
* as well, so the text never reaches a dictionary.
|
|
13
|
+
*/
|
|
14
|
+
export declare const PasswordInput: import("react").ForwardRefExoticComponent<PasswordInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { IconButton, TextField } from '@radix-ui/themes';
|
|
4
|
+
import { forwardRef, useState } from 'react';
|
|
5
|
+
import { Input } from '../../atoms/Input/Input.js';
|
|
6
|
+
import { useUiStrings } from '../../strings.js';
|
|
7
|
+
/**
|
|
8
|
+
* A password field with a show/hide toggle. The toggle is a pressed/unpressed button with one
|
|
9
|
+
* name (the `showPassword` UI string). Showing turns off spellcheck and autocapitalisation
|
|
10
|
+
* as well, so the text never reaches a dictionary.
|
|
11
|
+
*/
|
|
12
|
+
export const PasswordInput = forwardRef(function PasswordInput({ autoComplete, children, ...props }, ref) {
|
|
13
|
+
const strings = useUiStrings();
|
|
14
|
+
const [visible, setVisible] = useState(false);
|
|
15
|
+
return (_jsxs(Input, { ref: ref, type: visible ? 'text' : 'password', autoComplete: autoComplete, autoCapitalize: "none", autoCorrect: "off", spellCheck: false, ...props, children: [children, _jsx(TextField.Slot, { side: "right", children: _jsx(IconButton, { type: "button", size: "1", variant: "ghost", color: "gray", "aria-label": strings.showPassword, "aria-pressed": visible, onClick: () => setVisible((v) => !v), children: visible ? _jsx(EyeClosedIcon, {}) : _jsx(EyeOpenIcon, {}) }) })] }));
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PasswordInput.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './PasswordInput.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type InputSize } from '../../atoms/Input/Input.js';
|
|
2
|
+
export interface SearchFieldProps {
|
|
3
|
+
/** Controlled text. */
|
|
4
|
+
value?: string;
|
|
5
|
+
defaultValue?: string;
|
|
6
|
+
/** Called with the text, after `debounceMs` of no typing. Clearing reports at once. */
|
|
7
|
+
onValueChange: (value: string) => void;
|
|
8
|
+
/** Default 0 (every keystroke). 200–300 suits a search that hits a server. */
|
|
9
|
+
debounceMs?: number;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
/** The accessible name. Default: the `search` UI string. */
|
|
12
|
+
'aria-label'?: string;
|
|
13
|
+
/** A single key that focuses the field from anywhere on the page, e.g. `/`. */
|
|
14
|
+
shortcut?: string;
|
|
15
|
+
size?: InputSize;
|
|
16
|
+
id?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A search input with a magnifier, a clear button once there is text, Escape to clear, and an
|
|
21
|
+
* optional page-wide shortcut key. The shortcut hint is hidden on touch screens, which have no
|
|
22
|
+
* keyboard to press it on, and once the field has text.
|
|
23
|
+
*/
|
|
24
|
+
export declare function SearchField({ value, defaultValue, onValueChange, debounceMs, placeholder, 'aria-label': ariaLabel, shortcut, size, id, className, }: SearchFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Cross2Icon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { IconButton, Kbd, TextField } from '@radix-ui/themes';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { Input } from '../../atoms/Input/Input.js';
|
|
6
|
+
import { useUiStrings } from '../../strings.js';
|
|
7
|
+
function isEditable(el) {
|
|
8
|
+
if (!(el instanceof HTMLElement))
|
|
9
|
+
return false;
|
|
10
|
+
return el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A search input with a magnifier, a clear button once there is text, Escape to clear, and an
|
|
14
|
+
* optional page-wide shortcut key. The shortcut hint is hidden on touch screens, which have no
|
|
15
|
+
* keyboard to press it on, and once the field has text.
|
|
16
|
+
*/
|
|
17
|
+
export function SearchField({ value, defaultValue = '', onValueChange, debounceMs = 0, placeholder, 'aria-label': ariaLabel, shortcut, size, id, className, }) {
|
|
18
|
+
const strings = useUiStrings();
|
|
19
|
+
const [text, setText] = useState(value ?? defaultValue);
|
|
20
|
+
const inputRef = useRef(null);
|
|
21
|
+
const timer = useRef(undefined);
|
|
22
|
+
const report = useRef(onValueChange);
|
|
23
|
+
report.current = onValueChange;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (value !== undefined)
|
|
26
|
+
setText(value);
|
|
27
|
+
}, [value]);
|
|
28
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!shortcut)
|
|
31
|
+
return;
|
|
32
|
+
const onKey = (e) => {
|
|
33
|
+
if (e.key !== shortcut || e.metaKey || e.ctrlKey || e.altKey || isEditable(e.target))
|
|
34
|
+
return;
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
inputRef.current?.focus();
|
|
37
|
+
};
|
|
38
|
+
document.addEventListener('keydown', onKey);
|
|
39
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
40
|
+
}, [shortcut]);
|
|
41
|
+
const change = (next, immediate = false) => {
|
|
42
|
+
setText(next);
|
|
43
|
+
clearTimeout(timer.current);
|
|
44
|
+
if (immediate || debounceMs <= 0)
|
|
45
|
+
report.current(next);
|
|
46
|
+
else
|
|
47
|
+
timer.current = setTimeout(() => report.current(next), debounceMs);
|
|
48
|
+
};
|
|
49
|
+
return (_jsxs(Input, { ref: inputRef, id: id, type: "search", size: size, value: text, placeholder: placeholder, "aria-label": ariaLabel ?? strings.search, "aria-keyshortcuts": shortcut, className: className, onChange: (e) => change(e.target.value), onKeyDown: (e) => {
|
|
50
|
+
if (e.key === 'Escape' && text) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
change('', true);
|
|
53
|
+
}
|
|
54
|
+
}, children: [_jsx(TextField.Slot, { children: _jsx(MagnifyingGlassIcon, { "aria-hidden": "true" }) }), _jsx(TextField.Slot, { children: text ? (_jsx(IconButton, { type: "button", size: "1", variant: "ghost", color: "gray", "aria-label": strings.clearSearch, onClick: () => {
|
|
55
|
+
change('', true);
|
|
56
|
+
inputRef.current?.focus();
|
|
57
|
+
}, children: _jsx(Cross2Icon, {}) })) : shortcut ? (_jsx(Kbd, { className: "ui-search__kbd", "aria-hidden": "true", children: shortcut })) : null })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SearchField.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SearchField.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ScrollStripBreakpoint } from '../../atoms/ScrollStrip/ScrollStrip.js';
|
|
2
|
+
import { type StatTileProps } from '../../atoms/StatTile/StatTile.js';
|
|
3
|
+
export interface StatGroupItem extends StatTileProps {
|
|
4
|
+
/** React key. Defaults to the label when it is a string, else the index. */
|
|
5
|
+
id?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface StatGroupProps {
|
|
8
|
+
items: StatGroupItem[];
|
|
9
|
+
/** Grid columns from `gridFrom`. Default: the number of items, at most 4. */
|
|
10
|
+
columns?: number;
|
|
11
|
+
/** Below this the tiles scroll sideways; from it they are a grid. Default `md`. */
|
|
12
|
+
gridFrom?: ScrollStripBreakpoint;
|
|
13
|
+
/** Names the scrollable region (otherwise the `scrollableRegion` UI string). */
|
|
14
|
+
'aria-label'?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A row of KPI tiles: a swipeable strip on a phone, a grid from `gridFrom`. A `ScrollStrip` of
|
|
19
|
+
* `StatTile`s, so both keep their own behaviour and accessibility.
|
|
20
|
+
*/
|
|
21
|
+
export declare function StatGroup({ items, columns, gridFrom, 'aria-label': ariaLabel, className, }: StatGroupProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ScrollStrip } from '../../atoms/ScrollStrip/ScrollStrip.js';
|
|
3
|
+
import { StatTile } from '../../atoms/StatTile/StatTile.js';
|
|
4
|
+
/**
|
|
5
|
+
* A row of KPI tiles: a swipeable strip on a phone, a grid from `gridFrom`. A `ScrollStrip` of
|
|
6
|
+
* `StatTile`s, so both keep their own behaviour and accessibility.
|
|
7
|
+
*/
|
|
8
|
+
export function StatGroup({ items, columns, gridFrom = 'md', 'aria-label': ariaLabel, className, }) {
|
|
9
|
+
return (_jsx(ScrollStrip, { gridFrom: gridFrom, columns: columns ?? Math.min(items.length, 4), "aria-label": ariaLabel, className: className, children: items.map(({ id, ...tile }, i) => (_jsx(StatTile, { ...tile }, id ?? (typeof tile.label === 'string' ? tile.label : `stat-${i}`)))) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './StatGroup.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './StatGroup.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Theme } from '../../utils/theme.js';
|
|
2
|
+
/** The part of a `createTheming` instance the toggle uses. */
|
|
3
|
+
export interface ThemeStore {
|
|
4
|
+
readTheme(): Theme;
|
|
5
|
+
storeTheme(theme: Theme): void;
|
|
6
|
+
}
|
|
7
|
+
export interface ThemeToggleProps {
|
|
8
|
+
value?: Theme;
|
|
9
|
+
/** Initial value when uncontrolled. Default: `theming.readTheme()`, else `system`. */
|
|
10
|
+
defaultValue?: Theme;
|
|
11
|
+
onValueChange?: (theme: Theme) => void;
|
|
12
|
+
/** A `createTheming` instance: reads the stored choice and stores each new one. */
|
|
13
|
+
theming?: ThemeStore;
|
|
14
|
+
size?: 'sm' | 'md';
|
|
15
|
+
fullWidth?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* System / Light / Dark, as a segmented control.
|
|
19
|
+
*
|
|
20
|
+
* It chooses and remembers; it does not apply. Pass the value on to Radix's `<Theme
|
|
21
|
+
* appearance>` (or `applyTheme`) in `onValueChange`, because the app owns its `<Theme>`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ThemeToggle({ value, defaultValue, onValueChange, theming, size, fullWidth, }: ThemeToggleProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { SegmentedControl } from '../../atoms/SegmentedControl/SegmentedControl.js';
|
|
3
|
+
import { useControllableState } from '../../hooks/useControllableState.js';
|
|
4
|
+
import { useUiStrings } from '../../strings.js';
|
|
5
|
+
/**
|
|
6
|
+
* System / Light / Dark, as a segmented control.
|
|
7
|
+
*
|
|
8
|
+
* It chooses and remembers; it does not apply. Pass the value on to Radix's `<Theme
|
|
9
|
+
* appearance>` (or `applyTheme`) in `onValueChange`, because the app owns its `<Theme>`.
|
|
10
|
+
*/
|
|
11
|
+
export function ThemeToggle({ value, defaultValue, onValueChange, theming, size, fullWidth, }) {
|
|
12
|
+
const strings = useUiStrings();
|
|
13
|
+
const [current, setCurrent] = useControllableState({
|
|
14
|
+
value,
|
|
15
|
+
defaultValue: defaultValue ?? theming?.readTheme() ?? 'system',
|
|
16
|
+
onChange: (next) => {
|
|
17
|
+
theming?.storeTheme(next);
|
|
18
|
+
onValueChange?.(next);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
return (_jsx(SegmentedControl, { "aria-label": strings.appearance, size: size, fullWidth: fullWidth, value: current, onValueChange: (v) => setCurrent(v), options: [
|
|
22
|
+
{ value: 'system', label: strings.system },
|
|
23
|
+
{ value: 'light', label: strings.light },
|
|
24
|
+
{ value: 'dark', label: strings.dark },
|
|
25
|
+
] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ThemeToggle.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ThemeToggle.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ActionMenuItem } from '../../atoms/ActionMenu/ActionMenu.js';
|
|
2
|
+
import type { Breakpoint } from '../../hooks/breakpoints.js';
|
|
3
|
+
export interface UserMenuProps {
|
|
4
|
+
name: string;
|
|
5
|
+
/** Shown under the name in the sheet heading on small screens. */
|
|
6
|
+
email?: string;
|
|
7
|
+
avatarSrc?: string;
|
|
8
|
+
/** Profile, settings, sign out… Same shape as `ActionMenu`'s items. */
|
|
9
|
+
items: ActionMenuItem[];
|
|
10
|
+
onSelect: (id: string) => void;
|
|
11
|
+
/** Passed to ActionMenu. Default `sm`. */
|
|
12
|
+
sheetBelow?: Breakpoint | false;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The signed-in person's avatar and name, opening their account actions.
|
|
16
|
+
*
|
|
17
|
+
* Below 768px the name is visually hidden (still read out) and only the avatar shows, and the
|
|
18
|
+
* menu becomes a sheet headed by the name and email.
|
|
19
|
+
*/
|
|
20
|
+
export declare function UserMenu({ name, email, avatarSrc, items, onSelect, sheetBelow }: UserMenuProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ActionMenu } from '../../atoms/ActionMenu/ActionMenu.js';
|
|
3
|
+
import { Avatar } from '../../atoms/Avatar/Avatar.js';
|
|
4
|
+
import { useUiStrings } from '../../strings.js';
|
|
5
|
+
/**
|
|
6
|
+
* The signed-in person's avatar and name, opening their account actions.
|
|
7
|
+
*
|
|
8
|
+
* Below 768px the name is visually hidden (still read out) and only the avatar shows, and the
|
|
9
|
+
* menu becomes a sheet headed by the name and email.
|
|
10
|
+
*/
|
|
11
|
+
export function UserMenu({ name, email, avatarSrc, items, onSelect, sheetBelow }) {
|
|
12
|
+
const strings = useUiStrings();
|
|
13
|
+
return (_jsx(ActionMenu, { items: items, onSelect: onSelect, sheetBelow: sheetBelow, title: _jsxs(_Fragment, { children: [name, email ? _jsx("span", { className: "block text-sm font-normal text-ink-muted", children: email }) : null] }), trigger: _jsxs("button", { type: "button", className: "ui-user-menu__trigger", children: [_jsx("span", { "aria-hidden": "true", className: "inline-flex", children: _jsx(Avatar, { src: avatarSrc, name: name }) }), _jsx("span", { className: "ui-user-menu__name", children: name }), _jsxs("span", { className: "ui-sr-only", children: [", ", strings.userMenu] })] }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './UserMenu.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './UserMenu.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './AccentPicker/index.js';
|
|
2
|
+
export * from './Breadcrumbs/index.js';
|
|
3
|
+
export * from './Calendar/index.js';
|
|
4
|
+
export * from './DateInput/index.js';
|
|
5
|
+
export * from './EmptyState/index.js';
|
|
6
|
+
export * from './ErrorState/index.js';
|
|
7
|
+
export * from './ErrorSummary/index.js';
|
|
8
|
+
export * from './FilterChips/index.js';
|
|
9
|
+
export * from './FilterPopover/index.js';
|
|
10
|
+
export * from './FormActions/index.js';
|
|
11
|
+
export * from './FormField/index.js';
|
|
12
|
+
export * from './FormSection/index.js';
|
|
13
|
+
export * from './KeyValueList/index.js';
|
|
14
|
+
export * from './LoadingState/index.js';
|
|
15
|
+
export * from './NavItem/index.js';
|
|
16
|
+
export * from './Pagination/index.js';
|
|
17
|
+
export * from './PasswordInput/index.js';
|
|
18
|
+
export * from './SearchField/index.js';
|
|
19
|
+
export * from './StatGroup/index.js';
|
|
20
|
+
export * from './ThemeToggle/index.js';
|
|
21
|
+
export * from './UserMenu/index.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './AccentPicker/index.js';
|
|
2
|
+
export * from './Breadcrumbs/index.js';
|
|
3
|
+
export * from './Calendar/index.js';
|
|
4
|
+
export * from './DateInput/index.js';
|
|
5
|
+
export * from './EmptyState/index.js';
|
|
6
|
+
export * from './ErrorState/index.js';
|
|
7
|
+
export * from './ErrorSummary/index.js';
|
|
8
|
+
export * from './FilterChips/index.js';
|
|
9
|
+
export * from './FilterPopover/index.js';
|
|
10
|
+
export * from './FormActions/index.js';
|
|
11
|
+
export * from './FormField/index.js';
|
|
12
|
+
export * from './FormSection/index.js';
|
|
13
|
+
export * from './KeyValueList/index.js';
|
|
14
|
+
export * from './LoadingState/index.js';
|
|
15
|
+
export * from './NavItem/index.js';
|
|
16
|
+
export * from './Pagination/index.js';
|
|
17
|
+
export * from './PasswordInput/index.js';
|
|
18
|
+
export * from './SearchField/index.js';
|
|
19
|
+
export * from './StatGroup/index.js';
|
|
20
|
+
export * from './ThemeToggle/index.js';
|
|
21
|
+
export * from './UserMenu/index.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type TabBarProps } from '../../atoms/TabBar/TabBar.js';
|
|
3
|
+
import { type Breakpoint } from '../../hooks/breakpoints.js';
|
|
4
|
+
export interface AppShellProps {
|
|
5
|
+
/** A `Sidebar`. */
|
|
6
|
+
sidebar: ReactNode;
|
|
7
|
+
/** A `Topbar`, above the content. */
|
|
8
|
+
topbar?: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* A bottom TabBar for small screens, hidden from `breakpoint` up. Optional: the off-canvas
|
|
11
|
+
* sidebar is always available through the Topbar's menu button.
|
|
12
|
+
*/
|
|
13
|
+
mobileNav?: Omit<TabBarProps, 'hideFrom'>;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/** From here up the sidebar is a persistent column. Default `md` (1024px). */
|
|
16
|
+
breakpoint?: Breakpoint;
|
|
17
|
+
/** Controlled open state of the off-canvas sidebar. */
|
|
18
|
+
navOpen?: boolean;
|
|
19
|
+
onNavOpenChange?: (open: boolean) => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The admin frame: sidebar, topbar and content, with a skip link to the content.
|
|
24
|
+
*
|
|
25
|
+
* Wide screens get a two-column grid with a sticky sidebar. Narrow ones get one column, the
|
|
26
|
+
* sidebar off-canvas behind the Topbar's menu button, and optionally a bottom TabBar. The
|
|
27
|
+
* shell is at least the dynamic viewport's height, so the browser's own bars do not cut it off.
|
|
28
|
+
*/
|
|
29
|
+
export declare function AppShell({ sidebar, topbar, mobileNav, children, breakpoint, navOpen: navOpenProp, onNavOpenChange, className, }: AppShellProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useState } from 'react';
|
|
3
|
+
import { TabBar, TabBarSpacer } from '../../atoms/TabBar/TabBar.js';
|
|
4
|
+
import { minWidth } from '../../hooks/breakpoints.js';
|
|
5
|
+
import { useMediaQuery } from '../../hooks/useMediaQuery.js';
|
|
6
|
+
import { useUiStrings } from '../../strings.js';
|
|
7
|
+
import { ShellContext } from './context.js';
|
|
8
|
+
/**
|
|
9
|
+
* The admin frame: sidebar, topbar and content, with a skip link to the content.
|
|
10
|
+
*
|
|
11
|
+
* Wide screens get a two-column grid with a sticky sidebar. Narrow ones get one column, the
|
|
12
|
+
* sidebar off-canvas behind the Topbar's menu button, and optionally a bottom TabBar. The
|
|
13
|
+
* shell is at least the dynamic viewport's height, so the browser's own bars do not cut it off.
|
|
14
|
+
*/
|
|
15
|
+
export function AppShell({ sidebar, topbar, mobileNav, children, breakpoint = 'md', navOpen: navOpenProp, onNavOpenChange, className, }) {
|
|
16
|
+
const strings = useUiStrings();
|
|
17
|
+
const wide = useMediaQuery(minWidth(breakpoint));
|
|
18
|
+
const [ownOpen, setOwnOpen] = useState(false);
|
|
19
|
+
const mainId = useId();
|
|
20
|
+
const navOpen = navOpenProp ?? ownOpen;
|
|
21
|
+
const setNavOpen = (open) => {
|
|
22
|
+
if (navOpenProp === undefined)
|
|
23
|
+
setOwnOpen(open);
|
|
24
|
+
onNavOpenChange?.(open);
|
|
25
|
+
};
|
|
26
|
+
// Widening the window with the panel open closes it for good, rather than leaving it open
|
|
27
|
+
// behind the persistent sidebar to reappear the next time the window narrows.
|
|
28
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: setNavOpen is rebuilt every render; only these two values decide.
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (wide && navOpen)
|
|
31
|
+
setNavOpen(false);
|
|
32
|
+
}, [wide, navOpen]);
|
|
33
|
+
return (_jsx(ShellContext.Provider, { value: { breakpoint, wide, navOpen: navOpen && !wide, setNavOpen }, children: _jsxs("div", { className: ['ui-shell', className].filter(Boolean).join(' '), "data-wide": wide ? '' : undefined, "data-tabbar": mobileNav && !wide ? '' : undefined, children: [_jsx("a", { className: "ui-skip-link", href: `#${mainId}`, children: strings.skipToContent }), sidebar, _jsxs("div", { className: "ui-shell__main", children: [topbar, _jsx("main", { id: mainId, tabIndex: -1, className: "ui-shell__content", children: children }), mobileNav ? (_jsxs(_Fragment, { children: [_jsx(TabBarSpacer, { hideFrom: breakpoint }), _jsx(TabBar, { ...mobileNav, hideFrom: breakpoint })] })) : null] })] }) }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Breakpoint } from '../../hooks/breakpoints.js';
|
|
2
|
+
/**
|
|
3
|
+
* What AppShell shares with the Sidebar and Topbar inside it: whether the screen is wide enough
|
|
4
|
+
* for a persistent sidebar, and the open state of the off-canvas one when it is not.
|
|
5
|
+
*/
|
|
6
|
+
export interface ShellContextValue {
|
|
7
|
+
breakpoint: Breakpoint;
|
|
8
|
+
wide: boolean;
|
|
9
|
+
navOpen: boolean;
|
|
10
|
+
setNavOpen: (open: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const ShellContext: import("react").Context<ShellContextValue | null>;
|
|
13
|
+
/** `null` outside an AppShell; Sidebar and Topbar then manage themselves. */
|
|
14
|
+
export declare function useShell(): ShellContextValue | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AppShell.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AppShell.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type AuthFormBaseProps } from './shared.js';
|
|
3
|
+
export interface LoginValues {
|
|
4
|
+
identifier: string;
|
|
5
|
+
password: string;
|
|
6
|
+
remember: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface LoginFormProps extends AuthFormBaseProps<'identifier' | 'password'> {
|
|
9
|
+
onSubmit: (values: LoginValues) => void;
|
|
10
|
+
labels: {
|
|
11
|
+
/** Names the form. */
|
|
12
|
+
form: string;
|
|
13
|
+
identifier: string;
|
|
14
|
+
password: string;
|
|
15
|
+
submit: string;
|
|
16
|
+
/** With it, a "remember me" checkbox. */
|
|
17
|
+
remember?: string;
|
|
18
|
+
};
|
|
19
|
+
/** `email` (the default) or `username`. Both use `autocomplete="username"`. */
|
|
20
|
+
identifierType?: 'email' | 'username';
|
|
21
|
+
/** A "Forgot password?" link, shown under the password field. */
|
|
22
|
+
forgotPassword?: ReactNode;
|
|
23
|
+
defaultValues?: Partial<LoginValues>;
|
|
24
|
+
}
|
|
25
|
+
export declare function LoginForm({ onSubmit, labels, identifierType, forgotPassword, defaultValues, errors, formError, pending, footer, className, }: LoginFormProps): import("react").JSX.Element;
|
|
26
|
+
export interface SignupValues {
|
|
27
|
+
name: string;
|
|
28
|
+
email: string;
|
|
29
|
+
password: string;
|
|
30
|
+
acceptedTerms: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface SignupFormProps extends AuthFormBaseProps<'name' | 'email' | 'password' | 'acceptedTerms'> {
|
|
33
|
+
onSubmit: (values: SignupValues) => void;
|
|
34
|
+
labels: {
|
|
35
|
+
form: string;
|
|
36
|
+
/** With it, a name field. */
|
|
37
|
+
name?: string;
|
|
38
|
+
email: string;
|
|
39
|
+
password: string;
|
|
40
|
+
/** The rules, shown under the password field and linked to it. */
|
|
41
|
+
passwordHint?: string;
|
|
42
|
+
/** With it, a required checkbox; may contain links to the terms. */
|
|
43
|
+
terms?: ReactNode;
|
|
44
|
+
submit: string;
|
|
45
|
+
};
|
|
46
|
+
defaultValues?: Partial<SignupValues>;
|
|
47
|
+
}
|
|
48
|
+
export declare function SignupForm({ onSubmit, labels, defaultValues, errors, formError, pending, footer, className, }: SignupFormProps): import("react").JSX.Element;
|
|
49
|
+
export interface ForgotPasswordFormProps extends AuthFormBaseProps<'email'> {
|
|
50
|
+
onSubmit: (values: {
|
|
51
|
+
email: string;
|
|
52
|
+
}) => void;
|
|
53
|
+
labels: {
|
|
54
|
+
form: string;
|
|
55
|
+
email: string;
|
|
56
|
+
submit: string;
|
|
57
|
+
/** Shown once `sent`: "If that address has an account, a link is on its way." */
|
|
58
|
+
sent: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The request went out. Shows `labels.sent` as a status message and keeps the form, so the
|
|
62
|
+
* person can correct the address or ask again. Word `labels.sent` so it does not reveal
|
|
63
|
+
* whether the address has an account.
|
|
64
|
+
*/
|
|
65
|
+
sent?: boolean;
|
|
66
|
+
defaultEmail?: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function ForgotPasswordForm({ onSubmit, labels, sent, defaultEmail, errors, formError, pending, footer, className, }: ForgotPasswordFormProps): import("react").JSX.Element;
|
|
69
|
+
export interface ResetPasswordValues {
|
|
70
|
+
password: string;
|
|
71
|
+
confirmPassword: string;
|
|
72
|
+
}
|
|
73
|
+
export interface ResetPasswordFormProps extends AuthFormBaseProps<'password' | 'confirmPassword'> {
|
|
74
|
+
onSubmit: (values: ResetPasswordValues) => void;
|
|
75
|
+
labels: {
|
|
76
|
+
form: string;
|
|
77
|
+
password: string;
|
|
78
|
+
confirmPassword: string;
|
|
79
|
+
passwordHint?: string;
|
|
80
|
+
submit: string;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export declare function ResetPasswordForm({ onSubmit, labels, errors, formError, pending, footer, className, }: ResetPasswordFormProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Alert } from '../../atoms/Alert/Alert.js';
|
|
4
|
+
import { Checkbox } from '../../atoms/Checkbox/Checkbox.js';
|
|
5
|
+
import { Input } from '../../atoms/Input/Input.js';
|
|
6
|
+
import { FormField } from '../../molecules/FormField/FormField.js';
|
|
7
|
+
import { PasswordInput } from '../../molecules/PasswordInput/PasswordInput.js';
|
|
8
|
+
import { AuthFormShell, useAuthForm } from './shared.js';
|
|
9
|
+
const LOGIN_FIELDS = ['identifier', 'password'];
|
|
10
|
+
export function LoginForm({ onSubmit, labels, identifierType = 'email', forgotPassword, defaultValues, errors, formError, pending, footer, className, }) {
|
|
11
|
+
const { ids, markSubmitted } = useAuthForm(LOGIN_FIELDS, errors);
|
|
12
|
+
const [values, setValues] = useState({
|
|
13
|
+
identifier: '',
|
|
14
|
+
password: '',
|
|
15
|
+
remember: false,
|
|
16
|
+
...defaultValues,
|
|
17
|
+
});
|
|
18
|
+
const set = (k, v) => setValues((s) => ({ ...s, [k]: v }));
|
|
19
|
+
return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
|
|
20
|
+
markSubmitted();
|
|
21
|
+
onSubmit(values);
|
|
22
|
+
}, children: [_jsx(FormField, { id: ids.identifier, label: labels.identifier, required: true, error: errors?.identifier, children: _jsx(Input, { type: identifierType === 'email' ? 'email' : 'text', inputMode: identifierType === 'email' ? 'email' : undefined, autoComplete: "username", autoCapitalize: "none", spellCheck: false, value: values.identifier, onChange: (e) => set('identifier', e.target.value) }) }), _jsx(FormField, { id: ids.password, label: labels.password, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "current-password", value: values.password, onChange: (e) => set('password', e.target.value) }) }), forgotPassword || labels.remember ? (_jsxs("div", { className: "ui-auth-form__row", children: [labels.remember ? (_jsx(FormField, { label: labels.remember, layout: "inline", children: _jsx(Checkbox, { checked: values.remember, onCheckedChange: (c) => set('remember', c === true) }) })) : (_jsx("span", {})), forgotPassword] })) : null] }));
|
|
23
|
+
}
|
|
24
|
+
const SIGNUP_FIELDS = ['name', 'email', 'password', 'acceptedTerms'];
|
|
25
|
+
export function SignupForm({ onSubmit, labels, defaultValues, errors, formError, pending, footer, className, }) {
|
|
26
|
+
const { ids, markSubmitted } = useAuthForm(SIGNUP_FIELDS, errors);
|
|
27
|
+
const [values, setValues] = useState({
|
|
28
|
+
name: '',
|
|
29
|
+
email: '',
|
|
30
|
+
password: '',
|
|
31
|
+
acceptedTerms: false,
|
|
32
|
+
...defaultValues,
|
|
33
|
+
});
|
|
34
|
+
const set = (k, v) => setValues((s) => ({ ...s, [k]: v }));
|
|
35
|
+
return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
|
|
36
|
+
markSubmitted();
|
|
37
|
+
onSubmit(values);
|
|
38
|
+
}, children: [labels.name ? (_jsx(FormField, { id: ids.name, label: labels.name, required: true, error: errors?.name, children: _jsx(Input, { autoComplete: "name", value: values.name, onChange: (e) => set('name', e.target.value) }) })) : null, _jsx(FormField, { id: ids.email, label: labels.email, required: true, error: errors?.email, children: _jsx(Input, { type: "email", inputMode: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: false, value: values.email, onChange: (e) => set('email', e.target.value) }) }), _jsx(FormField, { id: ids.password, label: labels.password, hint: labels.passwordHint, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.password, onChange: (e) => set('password', e.target.value) }) }), labels.terms ? (_jsx(FormField, { id: ids.acceptedTerms, label: labels.terms, layout: "inline", required: true, error: errors?.acceptedTerms, children: _jsx(Checkbox, { checked: values.acceptedTerms, onCheckedChange: (c) => set('acceptedTerms', c === true) }) })) : null] }));
|
|
39
|
+
}
|
|
40
|
+
const FORGOT_FIELDS = ['email'];
|
|
41
|
+
export function ForgotPasswordForm({ onSubmit, labels, sent = false, defaultEmail = '', errors, formError, pending, footer, className, }) {
|
|
42
|
+
const { ids, markSubmitted } = useAuthForm(FORGOT_FIELDS, errors);
|
|
43
|
+
const [email, setEmail] = useState(defaultEmail);
|
|
44
|
+
return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
|
|
45
|
+
markSubmitted();
|
|
46
|
+
onSubmit({ email });
|
|
47
|
+
}, children: [sent ? _jsx(Alert, { type: "success", body: labels.sent }) : null, _jsx(FormField, { id: ids.email, label: labels.email, required: true, error: errors?.email, children: _jsx(Input, { type: "email", inputMode: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: false, value: email, onChange: (e) => setEmail(e.target.value) }) })] }));
|
|
48
|
+
}
|
|
49
|
+
const RESET_FIELDS = ['password', 'confirmPassword'];
|
|
50
|
+
export function ResetPasswordForm({ onSubmit, labels, errors, formError, pending, footer, className, }) {
|
|
51
|
+
const { ids, markSubmitted } = useAuthForm(RESET_FIELDS, errors);
|
|
52
|
+
const [values, setValues] = useState({ password: '', confirmPassword: '' });
|
|
53
|
+
return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
|
|
54
|
+
markSubmitted();
|
|
55
|
+
onSubmit(values);
|
|
56
|
+
}, children: [_jsx(FormField, { id: ids.password, label: labels.password, hint: labels.passwordHint, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.password, onChange: (e) => setValues((v) => ({ ...v, password: e.target.value })) }) }), _jsx(FormField, { id: ids.confirmPassword, label: labels.confirmPassword, required: true, error: errors?.confirmPassword, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.confirmPassword, onChange: (e) => setValues((v) => ({ ...v, confirmPassword: e.target.value })) }) })] }));
|
|
57
|
+
}
|