@kennycorrea/ui 0.11.4 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +191 -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 +34 -0
- package/dist/molecules/NavItem/NavItem.js +72 -0
- package/dist/molecules/NavItem/context.d.ts +19 -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 +120 -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 +73 -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 +500 -21
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MixerHorizontalIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { Popover as ThemedPopover } from '@radix-ui/themes';
|
|
4
|
+
import { Button } from '../../atoms/Button/Button.js';
|
|
5
|
+
import { Sheet } from '../../atoms/Sheet/Sheet.js';
|
|
6
|
+
import { minWidth } from '../../hooks/breakpoints.js';
|
|
7
|
+
import { useControllableState } from '../../hooks/useControllableState.js';
|
|
8
|
+
import { useMediaQuery } from '../../hooks/useMediaQuery.js';
|
|
9
|
+
import { useUiStrings } from '../../strings.js';
|
|
10
|
+
/**
|
|
11
|
+
* A "Filters" button that opens the filter controls: a popover anchored to the button on wide
|
|
12
|
+
* screens, a bottom sheet on narrow ones, where a popover would be cramped and hard to reach.
|
|
13
|
+
*/
|
|
14
|
+
export function FilterPopover({ label, activeCount = 0, children, onApply, onReset, open, defaultOpen = false, onOpenChange, sheetBelow = 'sm', }) {
|
|
15
|
+
const strings = useUiStrings();
|
|
16
|
+
const wide = useMediaQuery(minWidth(sheetBelow || 'xs'));
|
|
17
|
+
const [isOpen, setOpen] = useControllableState({
|
|
18
|
+
value: open,
|
|
19
|
+
defaultValue: defaultOpen,
|
|
20
|
+
onChange: onOpenChange,
|
|
21
|
+
});
|
|
22
|
+
const title = label ?? strings.filters;
|
|
23
|
+
const footer = onApply || onReset ? (_jsxs(_Fragment, { children: [onReset ? (_jsx(Button, { variant: "ghost", onClick: onReset, children: strings.reset })) : null, onApply ? (_jsx(Button, { onClick: () => {
|
|
24
|
+
onApply();
|
|
25
|
+
setOpen(false);
|
|
26
|
+
}, children: strings.apply })) : null] })) : null;
|
|
27
|
+
const trigger = (_jsxs(Button, { variant: "outline", onClick: sheetBelow !== false && !wide ? () => setOpen(true) : undefined, children: [_jsx(MixerHorizontalIcon, { "aria-hidden": "true" }), title, activeCount > 0 ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "ui-count", "aria-hidden": "true", children: activeCount }), _jsxs("span", { className: "ui-sr-only", children: [", ", strings.activeCount(activeCount)] })] })) : null] }));
|
|
28
|
+
if (sheetBelow !== false && !wide) {
|
|
29
|
+
return (_jsxs(_Fragment, { children: [trigger, _jsx(Sheet, { open: isOpen, onOpenChange: setOpen, title: title, footer: footer, showCloseButton: true, children: _jsx("div", { className: "flex flex-col gap-3", children: children }) })] }));
|
|
30
|
+
}
|
|
31
|
+
return (_jsxs(ThemedPopover.Root, { open: isOpen, onOpenChange: setOpen, children: [_jsx(ThemedPopover.Trigger, { children: trigger }), _jsxs(ThemedPopover.Content, { align: "start", className: "w-80 max-w-[calc(100vw-1.5rem)] p-0", "aria-label": title, children: [_jsx("div", { className: "ui-filter-popover__body", children: children }), footer ? _jsx("div", { className: "ui-filter-popover__footer", children: footer }) : null] })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FilterPopover.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FilterPopover.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface FormActionsProps {
|
|
3
|
+
/** The buttons, in reading order: cancel before save. */
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** Default `end`. `between` puts the first child at the start and the rest at the end. */
|
|
6
|
+
align?: 'start' | 'end' | 'between';
|
|
7
|
+
/**
|
|
8
|
+
* Below 768px, pins the row to the bottom of the screen on the bar material, with buttons
|
|
9
|
+
* sharing the width, so saving a long form never needs a scroll back down. It sits above an
|
|
10
|
+
* AppShell's TabBar. Default `true`.
|
|
11
|
+
*/
|
|
12
|
+
sticky?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/** The row of buttons that ends a form. */
|
|
16
|
+
export declare function FormActions({ children, align, sticky, className, }: FormActionsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/** The row of buttons that ends a form. */
|
|
3
|
+
export function FormActions({ children, align = 'end', sticky = true, className, }) {
|
|
4
|
+
return (_jsx("div", { className: ['ui-form-actions', className].filter(Boolean).join(' '), "data-align": align, "data-sticky": sticky ? '' : undefined, children: children }));
|
|
5
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FormActions.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FormActions.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
/** The ids and states a control needs, for wiring one up by hand with the render form. */
|
|
3
|
+
export interface FormFieldControlProps {
|
|
4
|
+
id: string;
|
|
5
|
+
'aria-describedby'?: string;
|
|
6
|
+
'aria-invalid'?: true;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface FormFieldProps {
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
/** Help that stays visible. Linked with `aria-describedby`. */
|
|
12
|
+
hint?: ReactNode;
|
|
13
|
+
/** The validation message. Marks the control invalid and is linked with `aria-describedby`. */
|
|
14
|
+
error?: ReactNode;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
/** Defaults to a generated id. */
|
|
17
|
+
id?: string;
|
|
18
|
+
/**
|
|
19
|
+
* `stacked` (label above, the default) or `inline` (control first, label beside it), which is
|
|
20
|
+
* the right shape for a Checkbox or Switch.
|
|
21
|
+
*/
|
|
22
|
+
layout?: 'stacked' | 'inline';
|
|
23
|
+
/**
|
|
24
|
+
* One control. Input, Textarea and Select get `invalid` (their red state); anything else gets
|
|
25
|
+
* `aria-invalid`. Or a function, given {@link FormFieldControlProps}, for a control that needs
|
|
26
|
+
* them placed somewhere else.
|
|
27
|
+
*/
|
|
28
|
+
children: ReactElement | ((control: FormFieldControlProps) => ReactNode);
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A label, a control, a hint and an error, wired together: the label names the control, the
|
|
33
|
+
* hint and error describe it, and the error marks it invalid.
|
|
34
|
+
*
|
|
35
|
+
* The error is text with an icon, never colour alone. It is not a live region: announcing each
|
|
36
|
+
* field's error as it appears is noisy, and `ErrorSummary` says it once, on submit.
|
|
37
|
+
*/
|
|
38
|
+
export declare function FormField({ label, hint, error, required, id: idProp, layout, children, className, }: FormFieldProps): import("react").JSX.Element;
|
|
39
|
+
export interface FormFieldsetProps {
|
|
40
|
+
/** Names the group: "Notifications", "Plan". */
|
|
41
|
+
legend: ReactNode;
|
|
42
|
+
hint?: ReactNode;
|
|
43
|
+
error?: ReactNode;
|
|
44
|
+
/** Checkboxes, a RadioGroup, several related fields. */
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A `fieldset` for controls that answer one question together. The legend names the group;
|
|
50
|
+
* hint and error describe it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function FormFieldset({ legend, hint, error, children, className }: FormFieldsetProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { cloneElement, isValidElement, useId } from 'react';
|
|
4
|
+
import { Input } from '../../atoms/Input/Input.js';
|
|
5
|
+
import { Label } from '../../atoms/Label/Label.js';
|
|
6
|
+
import { Select } from '../../atoms/Select/Select.js';
|
|
7
|
+
import { Textarea } from '../../atoms/Textarea/Textarea.js';
|
|
8
|
+
import { PasswordInput } from '../PasswordInput/PasswordInput.js';
|
|
9
|
+
const INVALID_PROP = new Set([Input, Textarea, Select, PasswordInput]);
|
|
10
|
+
/**
|
|
11
|
+
* A label, a control, a hint and an error, wired together: the label names the control, the
|
|
12
|
+
* hint and error describe it, and the error marks it invalid.
|
|
13
|
+
*
|
|
14
|
+
* The error is text with an icon, never colour alone. It is not a live region: announcing each
|
|
15
|
+
* field's error as it appears is noisy, and `ErrorSummary` says it once, on submit.
|
|
16
|
+
*/
|
|
17
|
+
export function FormField({ label, hint, error, required, id: idProp, layout = 'stacked', children, className, }) {
|
|
18
|
+
const auto = useId();
|
|
19
|
+
const id = idProp ?? auto;
|
|
20
|
+
const hintId = `${id}-hint`;
|
|
21
|
+
const errorId = `${id}-error`;
|
|
22
|
+
const describedBy = [hint ? hintId : null, error ? errorId : null].filter(Boolean).join(' ');
|
|
23
|
+
const control = {
|
|
24
|
+
id,
|
|
25
|
+
'aria-describedby': describedBy || undefined,
|
|
26
|
+
'aria-invalid': error ? true : undefined,
|
|
27
|
+
required: required || undefined,
|
|
28
|
+
};
|
|
29
|
+
let rendered;
|
|
30
|
+
if (typeof children === 'function') {
|
|
31
|
+
rendered = children(control);
|
|
32
|
+
}
|
|
33
|
+
else if (isValidElement(children)) {
|
|
34
|
+
// The house fields set `aria-invalid` themselves from `invalid`, along with their red state.
|
|
35
|
+
// Passing the key at all, even as undefined, would override theirs, so it is left out.
|
|
36
|
+
const { 'aria-invalid': ariaInvalid, ...rest } = control;
|
|
37
|
+
const props = INVALID_PROP.has(children.type)
|
|
38
|
+
? { ...rest, invalid: !!error }
|
|
39
|
+
: { ...rest, ...(ariaInvalid ? { 'aria-invalid': ariaInvalid } : {}) };
|
|
40
|
+
rendered = cloneElement(children, props);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
rendered = children;
|
|
44
|
+
}
|
|
45
|
+
const labelEl = (_jsx(Label, { htmlFor: id, required: required, children: label }));
|
|
46
|
+
const hintEl = hint ? (_jsx("p", { id: hintId, className: "ui-field__hint", children: hint })) : null;
|
|
47
|
+
const errorEl = error ? _jsx(FieldError, { id: errorId, children: error }) : null;
|
|
48
|
+
return (_jsx("div", { className: ['ui-field', className].filter(Boolean).join(' '), "data-layout": layout, children: layout === 'inline' ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "ui-field__control", children: rendered }), _jsxs("span", { className: "ui-field__text", children: [labelEl, hintEl, errorEl] })] })) : (_jsxs(_Fragment, { children: [labelEl, hintEl, rendered, errorEl] })) }));
|
|
49
|
+
}
|
|
50
|
+
function FieldError({ id, children }) {
|
|
51
|
+
return (_jsxs("p", { id: id, className: "ui-field__error", children: [_jsx(ExclamationTriangleIcon, { "aria-hidden": "true" }), _jsx("span", { children: children })] }));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A `fieldset` for controls that answer one question together. The legend names the group;
|
|
55
|
+
* hint and error describe it.
|
|
56
|
+
*/
|
|
57
|
+
export function FormFieldset({ legend, hint, error, children, className }) {
|
|
58
|
+
const id = useId();
|
|
59
|
+
const describedBy = [hint ? `${id}-hint` : null, error ? `${id}-error` : null]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(' ');
|
|
62
|
+
return (_jsxs("fieldset", { className: ['ui-fieldset', className].filter(Boolean).join(' '), "aria-describedby": describedBy || undefined, children: [_jsx("legend", { className: "ui-fieldset__legend", children: legend }), hint ? (_jsx("p", { id: `${id}-hint`, className: "ui-field__hint", children: hint })) : null, children, error ? _jsx(FieldError, { id: `${id}-error`, children: error }) : null] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FormField.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FormField.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface FormSectionProps {
|
|
3
|
+
title: ReactNode;
|
|
4
|
+
/** Why these fields matter, or what they change. */
|
|
5
|
+
description?: ReactNode;
|
|
6
|
+
/** FormFields. */
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A titled group of fields: a `section` named by its `h2`. From 720px of its own width the
|
|
12
|
+
* title and description sit in a column beside the fields; narrower, above them.
|
|
13
|
+
*/
|
|
14
|
+
export declare function FormSection({ title, description, children, className }: FormSectionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* A titled group of fields: a `section` named by its `h2`. From 720px of its own width the
|
|
5
|
+
* title and description sit in a column beside the fields; narrower, above them.
|
|
6
|
+
*/
|
|
7
|
+
export function FormSection({ title, description, children, className }) {
|
|
8
|
+
const titleId = useId();
|
|
9
|
+
return (_jsx("section", { "aria-labelledby": titleId, className: ['ui-form-section', className].filter(Boolean).join(' '), children: _jsxs("div", { className: "ui-form-section__grid", children: [_jsxs("div", { children: [_jsx("h2", { id: titleId, className: "ui-form-section__title", children: title }), description ? _jsx("p", { className: "ui-form-section__description", children: description }) : null] }), _jsx("div", { className: "ui-form-section__fields", children: children })] }) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FormSection.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FormSection.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Responsive } from '../../atoms/Card/Card.js';
|
|
3
|
+
export interface KeyValueItem {
|
|
4
|
+
/** React key. Defaults to the label when it is a string. */
|
|
5
|
+
id?: string;
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
value: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export interface KeyValueListProps {
|
|
10
|
+
items: KeyValueItem[];
|
|
11
|
+
/**
|
|
12
|
+
* Label beside value, or above it. Default: above on phones, beside from `sm` (768px), in
|
|
13
|
+
* Radix's responsive-prop shape.
|
|
14
|
+
*/
|
|
15
|
+
orientation?: Responsive<'horizontal' | 'vertical'>;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Labelled facts about one thing: the side panel of a detail page. Radix Themes' `DataList`
|
|
20
|
+
* (a description list underneath), with items as data and a phone-first default orientation.
|
|
21
|
+
*/
|
|
22
|
+
export declare function KeyValueList({ items, orientation, className, }: KeyValueListProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DataList } from '@radix-ui/themes';
|
|
3
|
+
/**
|
|
4
|
+
* Labelled facts about one thing: the side panel of a detail page. Radix Themes' `DataList`
|
|
5
|
+
* (a description list underneath), with items as data and a phone-first default orientation.
|
|
6
|
+
*/
|
|
7
|
+
export function KeyValueList({ items, orientation = { initial: 'vertical', sm: 'horizontal' }, className, }) {
|
|
8
|
+
return (_jsx(DataList.Root, { orientation: orientation, className: className, children: items.map((item, i) => (_jsxs(DataList.Item, { children: [_jsx(DataList.Label, { children: item.label }), _jsx(DataList.Value, { children: item.value })] }, item.id ?? (typeof item.label === 'string' ? item.label : `kv-${i}`)))) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './KeyValueList.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './KeyValueList.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type LoadingStateVariant = 'spinner' | 'rows' | 'cards';
|
|
2
|
+
export interface LoadingStateProps {
|
|
3
|
+
/**
|
|
4
|
+
* `spinner` when the shape of what is coming is unknown; `rows` and `cards` draw its shape,
|
|
5
|
+
* so nothing jumps when it arrives. Default `spinner`.
|
|
6
|
+
*/
|
|
7
|
+
variant?: LoadingStateVariant;
|
|
8
|
+
/** How many rows or cards to draw. Default 5 rows, 3 cards. */
|
|
9
|
+
count?: number;
|
|
10
|
+
/** Overrides the announced text (otherwise the `loading` UI string). */
|
|
11
|
+
label?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A placeholder while something loads. Announced once (`role="status"`); the skeleton shapes
|
|
16
|
+
* themselves are hidden from screen readers.
|
|
17
|
+
*
|
|
18
|
+
* For a table, `DataTable state="loading"` draws skeleton rows inside the table itself.
|
|
19
|
+
*/
|
|
20
|
+
export declare function LoadingState({ variant, count, label, className }: LoadingStateProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Skeleton as ThemedSkeleton } from '@radix-ui/themes';
|
|
3
|
+
import { Card } from '../../atoms/Card/Card.js';
|
|
4
|
+
import { SkeletonText } from '../../atoms/Skeleton/Skeleton.js';
|
|
5
|
+
import { Spinner } from '../../atoms/Spinner/Spinner.js';
|
|
6
|
+
import { useUiStrings } from '../../strings.js';
|
|
7
|
+
/**
|
|
8
|
+
* A placeholder while something loads. Announced once (`role="status"`); the skeleton shapes
|
|
9
|
+
* themselves are hidden from screen readers.
|
|
10
|
+
*
|
|
11
|
+
* For a table, `DataTable state="loading"` draws skeleton rows inside the table itself.
|
|
12
|
+
*/
|
|
13
|
+
export function LoadingState({ variant = 'spinner', count, label, className }) {
|
|
14
|
+
const strings = useUiStrings();
|
|
15
|
+
const cls = ['ui-loading', className].filter(Boolean).join(' ');
|
|
16
|
+
if (variant === 'spinner') {
|
|
17
|
+
return (_jsx("div", { className: cls, "data-variant": "spinner", children: _jsx(Spinner, { label: label }) }));
|
|
18
|
+
}
|
|
19
|
+
const n = count ?? (variant === 'rows' ? 5 : 3);
|
|
20
|
+
const keys = Array.from({ length: n }, (_, i) => `k${i}`);
|
|
21
|
+
return (_jsx("div", { className: cls, "data-variant": variant, role: "status", "aria-label": label ?? strings.loading, children: keys.map((key) => variant === 'rows' ? (_jsxs("div", { "aria-hidden": "true", className: "flex items-center gap-3", children: [_jsx(ThemedSkeleton, { width: "32px", height: "32px", style: { borderRadius: 8 } }), _jsxs("div", { className: "flex flex-1 flex-col gap-2", children: [_jsx(ThemedSkeleton, { height: "0.875rem", width: "55%" }), _jsx(ThemedSkeleton, { height: "0.75rem", width: "35%" })] })] }, key)) : (_jsx(Card, { "aria-hidden": "true", className: "p-4", children: _jsx(SkeletonText, { lines: 3 }) }, key))) }));
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LoadingState.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LoadingState.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { RenderLink } from '../../atoms/TextLink/TextLink.js';
|
|
3
|
+
export interface NavItemProps {
|
|
4
|
+
/** React key when the item is rendered from data. Defaults to `label`. */
|
|
5
|
+
id?: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
/** A link. Without it (and without `items`) the item is a button. */
|
|
9
|
+
href?: string;
|
|
10
|
+
/** Wraps a router link. Receives the props to spread, like every `render` in this library. */
|
|
11
|
+
render?: RenderLink;
|
|
12
|
+
/** The current page: highlighted, and `aria-current="page"`. */
|
|
13
|
+
active?: boolean;
|
|
14
|
+
/** A number shows a count (99+ caps); `true` shows a dot. */
|
|
15
|
+
badge?: number | boolean;
|
|
16
|
+
/** What the badge means, for screen readers ("3 unread"). A dot is silent without it. */
|
|
17
|
+
badgeLabel?: string;
|
|
18
|
+
onClick?: () => void;
|
|
19
|
+
/** Child items: the item becomes a disclosure that shows them. One level deep. */
|
|
20
|
+
items?: NavItemProps[];
|
|
21
|
+
/** Whether a group starts open. Default: open when one of its children is active. */
|
|
22
|
+
defaultOpen?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** The key a group is tracked by in the Sidebar's accordion. */
|
|
25
|
+
export declare function navKey(item: NavItemProps): string;
|
|
26
|
+
/**
|
|
27
|
+
* One entry in a navigation list: a link, a button, or a group of child links.
|
|
28
|
+
*
|
|
29
|
+
* Renders an `<li>`, so put it inside a `<ul>` (Sidebar does). Inside a collapsed Sidebar the
|
|
30
|
+
* label stays in the accessible name and is also shown as a tooltip. Inside a Sidebar groups are
|
|
31
|
+
* an accordion: opening one closes the others, and choosing one from the icon rail expands the
|
|
32
|
+
* sidebar with that group open. Outside one, each group opens on its own.
|
|
33
|
+
*/
|
|
34
|
+
export declare function NavItem(item: NavItemProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { useId, useState } from 'react';
|
|
4
|
+
import { Tooltip } from '../../atoms/Tooltip/Tooltip.js';
|
|
5
|
+
import { useNavContext } from './context.js';
|
|
6
|
+
function badgeText(badge) {
|
|
7
|
+
if (badge === undefined || badge === false || badge === 0)
|
|
8
|
+
return null;
|
|
9
|
+
if (badge === true)
|
|
10
|
+
return '';
|
|
11
|
+
return badge > 99 ? '99+' : String(badge);
|
|
12
|
+
}
|
|
13
|
+
/** The inside of an item: icon, label, badge. Shared by links, buttons and group toggles. */
|
|
14
|
+
function ItemContent({ item, trailing }) {
|
|
15
|
+
const text = badgeText(item.badge);
|
|
16
|
+
return (_jsxs(_Fragment, { children: [item.icon ? (_jsx("span", { className: "ui-nav-item__icon", "aria-hidden": "true", children: item.icon })) : null, _jsx("span", { className: "ui-nav-item__label", children: item.label }), text !== null ? (_jsx("span", { className: "ui-nav-item__badge", "data-dot": text === '' ? '' : undefined, "aria-hidden": item.badgeLabel ? 'true' : text === '' ? 'true' : undefined, children: text })) : null, item.badgeLabel && text !== null ? (_jsxs("span", { className: "ui-sr-only", children: [", ", item.badgeLabel] })) : null, trailing] }));
|
|
17
|
+
}
|
|
18
|
+
/** A link or a button, depending on what the item has. */
|
|
19
|
+
function ItemControl({ item, onNavigate }) {
|
|
20
|
+
const onClick = () => {
|
|
21
|
+
item.onClick?.();
|
|
22
|
+
onNavigate?.();
|
|
23
|
+
};
|
|
24
|
+
const children = _jsx(ItemContent, { item: item });
|
|
25
|
+
if (item.href !== undefined) {
|
|
26
|
+
const props = {
|
|
27
|
+
href: item.href,
|
|
28
|
+
className: 'ui-nav-item',
|
|
29
|
+
'aria-current': item.active ? 'page' : undefined,
|
|
30
|
+
onClick,
|
|
31
|
+
children,
|
|
32
|
+
};
|
|
33
|
+
return item.render ? item.render(props) : _jsx("a", { ...props });
|
|
34
|
+
}
|
|
35
|
+
return (_jsx("button", { type: "button", className: "ui-nav-item", "aria-current": item.active ? 'page' : undefined, onClick: onClick, children: children }));
|
|
36
|
+
}
|
|
37
|
+
/** The key a group is tracked by in the Sidebar's accordion. */
|
|
38
|
+
export function navKey(item) {
|
|
39
|
+
return item.id ?? item.label;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One entry in a navigation list: a link, a button, or a group of child links.
|
|
43
|
+
*
|
|
44
|
+
* Renders an `<li>`, so put it inside a `<ul>` (Sidebar does). Inside a collapsed Sidebar the
|
|
45
|
+
* label stays in the accessible name and is also shown as a tooltip. Inside a Sidebar groups are
|
|
46
|
+
* an accordion: opening one closes the others, and choosing one from the icon rail expands the
|
|
47
|
+
* sidebar with that group open. Outside one, each group opens on its own.
|
|
48
|
+
*/
|
|
49
|
+
export function NavItem(item) {
|
|
50
|
+
const { collapsed, onNavigate, openGroup, setOpenGroup, expand } = useNavContext();
|
|
51
|
+
const childActive = item.items?.some((c) => c.active) ?? false;
|
|
52
|
+
const [ownOpen, setOwnOpen] = useState(item.defaultOpen ?? childActive);
|
|
53
|
+
const listId = useId();
|
|
54
|
+
const key = navKey(item);
|
|
55
|
+
const open = setOpenGroup ? openGroup === key : ownOpen;
|
|
56
|
+
const setOpen = (next) => {
|
|
57
|
+
if (setOpenGroup)
|
|
58
|
+
setOpenGroup(next ? key : null);
|
|
59
|
+
else
|
|
60
|
+
setOwnOpen(next);
|
|
61
|
+
};
|
|
62
|
+
if (!item.items) {
|
|
63
|
+
return (_jsx("li", { children: _jsx(Tooltip, { label: collapsed ? item.label : undefined, children: _jsx(ItemControl, { item: item, onNavigate: onNavigate }) }) }));
|
|
64
|
+
}
|
|
65
|
+
if (collapsed) {
|
|
66
|
+
return (_jsx("li", { children: _jsx(Tooltip, { label: item.label, children: _jsx("button", { type: "button", className: "ui-nav-item", "aria-current": childActive ? 'page' : undefined, "aria-expanded": false, onClick: () => {
|
|
67
|
+
setOpen(true);
|
|
68
|
+
expand?.();
|
|
69
|
+
}, children: _jsx(ItemContent, { item: item }) }) }) }));
|
|
70
|
+
}
|
|
71
|
+
return (_jsxs("li", { children: [_jsx("button", { type: "button", className: "ui-nav-item", "aria-expanded": open, "aria-controls": listId, onClick: () => setOpen(!open), children: _jsx(ItemContent, { item: item, trailing: _jsx("span", { className: "ui-nav-item__chevron", "aria-hidden": "true", children: _jsx(ChevronDownIcon, {}) }) }) }), _jsx("div", { className: "ui-nav-group", "data-open": open ? '' : undefined, children: _jsx("ul", { id: listId, className: "ui-nav-group__items", inert: !open, children: item.items.map((child) => (_jsx("li", { children: _jsx(ItemControl, { item: child, onNavigate: onNavigate }) }, child.id ?? child.label))) }) })] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a NavItem needs from the navigation around it. Provided by `Sidebar`; a NavItem outside
|
|
3
|
+
* one gets the defaults (expanded, nothing to close).
|
|
4
|
+
*
|
|
5
|
+
* Not exported from the package: it is the contract between the two components, not API.
|
|
6
|
+
*/
|
|
7
|
+
export interface NavContextValue {
|
|
8
|
+
/** The sidebar is an icon rail: labels become tooltips. */
|
|
9
|
+
collapsed: boolean;
|
|
10
|
+
/** Called after any item is activated, so an off-canvas panel can close. */
|
|
11
|
+
onNavigate?: () => void;
|
|
12
|
+
/** The one group that is open, by key. Sidebar keeps it so groups behave as an accordion. */
|
|
13
|
+
openGroup?: string | null;
|
|
14
|
+
setOpenGroup?: (key: string | null) => void;
|
|
15
|
+
/** Leaves the icon rail, so a group chosen there can open in place. */
|
|
16
|
+
expand?: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const NavContext: import("react").Context<NavContextValue>;
|
|
19
|
+
export declare function useNavContext(): NavContextValue;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './NavItem.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './NavItem.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type PaginationEntry = number | 'gap-start' | 'gap-end';
|
|
2
|
+
/**
|
|
3
|
+
* The page numbers to show: always the first and last, `siblings` either side of the current
|
|
4
|
+
* one, and a gap marker wherever pages are skipped. A gap of exactly one page shows that page
|
|
5
|
+
* instead, since "…" would take the same room and say less; for the same reason a short run
|
|
6
|
+
* (up to `2 * siblings + 5` pages) shows every page.
|
|
7
|
+
*/
|
|
8
|
+
export declare function paginationRange(page: number, pageCount: number, siblings?: number): PaginationEntry[];
|
|
9
|
+
export interface PaginationProps {
|
|
10
|
+
/** 1-based. */
|
|
11
|
+
page: number;
|
|
12
|
+
onPageChange: (page: number) => void;
|
|
13
|
+
/** Either this, or `total` with `pageSize`. */
|
|
14
|
+
pageCount?: number;
|
|
15
|
+
/** The number of items across all pages. With `pageSize`, also shows "1–25 of 500". */
|
|
16
|
+
total?: number;
|
|
17
|
+
pageSize?: number;
|
|
18
|
+
/** Shows a rows-per-page select. Needs `onPageSizeChange`. */
|
|
19
|
+
pageSizeOptions?: number[];
|
|
20
|
+
onPageSizeChange?: (size: number) => void;
|
|
21
|
+
/** Pages shown either side of the current one. Default 1. */
|
|
22
|
+
siblings?: number;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Page navigation for a list or table, controlled: it never changes page by itself.
|
|
27
|
+
*
|
|
28
|
+
* A `nav` landmark. At 520px of its own width or more it shows numbered pages; narrower, just
|
|
29
|
+
* previous / "Page 2 of 10" / next, and the rows-per-page select is dropped. Both are in the
|
|
30
|
+
* markup and CSS shows one, so nothing re-renders on resize.
|
|
31
|
+
*/
|
|
32
|
+
export declare function Pagination({ page, onPageChange, pageCount: pageCountProp, total, pageSize, pageSizeOptions, onPageSizeChange, siblings, className, }: PaginationProps): import("react").JSX.Element;
|
|
@@ -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';
|