@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,25 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface ErrorSummaryItem {
|
|
3
|
+
/** The id of the control with the problem (FormField's `id`). */
|
|
4
|
+
fieldId: string;
|
|
5
|
+
message: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export interface ErrorSummaryProps {
|
|
8
|
+
errors: ErrorSummaryItem[];
|
|
9
|
+
/**
|
|
10
|
+
* Increment on each submit. The summary takes focus whenever this changes while there are
|
|
11
|
+
* errors, so a second failed submit moves focus back to it.
|
|
12
|
+
*/
|
|
13
|
+
submitCount: number;
|
|
14
|
+
/** Default: the `fixErrors` UI string. */
|
|
15
|
+
title?: ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The list of a form's problems, shown above the form after a failed submit. It takes focus,
|
|
20
|
+
* so a screen reader reads the heading and the count, and every item links to its field:
|
|
21
|
+
* following one focuses the control itself, not just scrolls to it.
|
|
22
|
+
*
|
|
23
|
+
* Renders nothing while there are no errors.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ErrorSummary({ errors, submitCount, title, className }: ErrorSummaryProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef } from 'react';
|
|
3
|
+
import { useUiStrings } from '../../strings.js';
|
|
4
|
+
/**
|
|
5
|
+
* The list of a form's problems, shown above the form after a failed submit. It takes focus,
|
|
6
|
+
* so a screen reader reads the heading and the count, and every item links to its field:
|
|
7
|
+
* following one focuses the control itself, not just scrolls to it.
|
|
8
|
+
*
|
|
9
|
+
* Renders nothing while there are no errors.
|
|
10
|
+
*/
|
|
11
|
+
export function ErrorSummary({ errors, submitCount, title, className }) {
|
|
12
|
+
const strings = useUiStrings();
|
|
13
|
+
const ref = useRef(null);
|
|
14
|
+
const titleId = useId();
|
|
15
|
+
const hasErrors = errors.length > 0;
|
|
16
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: focus follows submits, not every change to the list.
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (hasErrors)
|
|
19
|
+
ref.current?.focus();
|
|
20
|
+
}, [submitCount]);
|
|
21
|
+
if (!hasErrors)
|
|
22
|
+
return null;
|
|
23
|
+
const focusField = (e, fieldId) => {
|
|
24
|
+
const target = document.getElementById(fieldId);
|
|
25
|
+
if (!target)
|
|
26
|
+
return;
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
target.focus();
|
|
29
|
+
target.scrollIntoView?.({ block: 'center' });
|
|
30
|
+
};
|
|
31
|
+
return (_jsxs("section", { ref: ref, tabIndex: -1, "aria-labelledby": titleId, className: ['ui-error-summary', className].filter(Boolean).join(' '), children: [_jsx("h2", { id: titleId, className: "ui-error-summary__title", children: title ?? strings.fixErrors(errors.length) }), _jsx("ul", { className: "ui-error-summary__list", children: errors.map((error) => (_jsx("li", { children: _jsx("a", { href: `#${error.fieldId}`, onClick: (e) => focusField(e, error.fieldId), children: error.message }) }, error.fieldId))) })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ErrorSummary.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ErrorSummary.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ChipGroupProps } from '../../atoms/ChipGroup/ChipGroup.js';
|
|
2
|
+
export type FilterChipsProps = ChipGroupProps & {
|
|
3
|
+
/** Shows a "Clear all" button while anything is selected. */
|
|
4
|
+
onClear?: () => void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Quick filters as chips: a `ChipGroup` with a clear-all beside it. Every ChipGroup behaviour
|
|
8
|
+
* (single or multiple, scrolling with fade edges, arrow keys) is unchanged.
|
|
9
|
+
*/
|
|
10
|
+
export declare function FilterChips({ onClear, ...group }: FilterChipsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '../../atoms/Button/Button.js';
|
|
3
|
+
import { ChipGroup } from '../../atoms/ChipGroup/ChipGroup.js';
|
|
4
|
+
import { useUiStrings } from '../../strings.js';
|
|
5
|
+
/**
|
|
6
|
+
* Quick filters as chips: a `ChipGroup` with a clear-all beside it. Every ChipGroup behaviour
|
|
7
|
+
* (single or multiple, scrolling with fade edges, arrow keys) is unchanged.
|
|
8
|
+
*/
|
|
9
|
+
export function FilterChips({ onClear, ...group }) {
|
|
10
|
+
const strings = useUiStrings();
|
|
11
|
+
const active = group.multiple ? group.value.length > 0 : group.value !== null;
|
|
12
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(ChipGroup, { ...group }) }), onClear && active ? (_jsx(Button, { variant: "ghost", size: "sm", onClick: onClear, children: strings.clearFilters })) : null] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FilterChips.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './FilterChips.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { type Breakpoint } from '../../hooks/breakpoints.js';
|
|
3
|
+
export interface FilterPopoverProps {
|
|
4
|
+
/** The trigger's text and the sheet's title. Default: the `filters` UI string. */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** How many filters are set. Shown as a count on the trigger and read with it. */
|
|
7
|
+
activeCount?: number;
|
|
8
|
+
/** The filter controls: FormFields, a FormFieldset of checkboxes. */
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* With this, the controls are a draft: Apply calls it and closes. Without it, the controls
|
|
12
|
+
* apply as they change and there is no Apply button.
|
|
13
|
+
*/
|
|
14
|
+
onApply?: () => void;
|
|
15
|
+
/** Shows a Reset button. */
|
|
16
|
+
onReset?: () => void;
|
|
17
|
+
open?: boolean;
|
|
18
|
+
defaultOpen?: boolean;
|
|
19
|
+
onOpenChange?: (open: boolean) => void;
|
|
20
|
+
/** Below this the panel is a bottom Sheet. Default `sm` (768px). `false` keeps the popover. */
|
|
21
|
+
sheetBelow?: Breakpoint | false;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A "Filters" button that opens the filter controls: a popover anchored to the button on wide
|
|
25
|
+
* screens, a bottom sheet on narrow ones, where a popover would be cramped and hard to reach.
|
|
26
|
+
*/
|
|
27
|
+
export declare function FilterPopover({ label, activeCount, children, onApply, onReset, open, defaultOpen, onOpenChange, sheetBelow, }: FilterPopoverProps): import("react").JSX.Element;
|
|
@@ -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,31 @@
|
|
|
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
|
+
/**
|
|
25
|
+
* One entry in a navigation list: a link, a button, or a group of child links.
|
|
26
|
+
*
|
|
27
|
+
* Renders an `<li>`, so put it inside a `<ul>` (Sidebar does). Inside a collapsed Sidebar the
|
|
28
|
+
* label stays in the accessible name and is also shown as a tooltip, and a group opens its
|
|
29
|
+
* children as a menu beside the rail, since there is no room to expand it in place.
|
|
30
|
+
*/
|
|
31
|
+
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 { DropdownMenu as ThemedDropdownMenu } from '@radix-ui/themes';
|
|
4
|
+
import { useId, useState } from 'react';
|
|
5
|
+
import { Tooltip } from '../../atoms/Tooltip/Tooltip.js';
|
|
6
|
+
import { useNavContext } from './context.js';
|
|
7
|
+
function badgeText(badge) {
|
|
8
|
+
if (badge === undefined || badge === false || badge === 0)
|
|
9
|
+
return null;
|
|
10
|
+
if (badge === true)
|
|
11
|
+
return '';
|
|
12
|
+
return badge > 99 ? '99+' : String(badge);
|
|
13
|
+
}
|
|
14
|
+
/** The inside of an item: icon, label, badge. Shared by links, buttons and group toggles. */
|
|
15
|
+
function ItemContent({ item, trailing }) {
|
|
16
|
+
const text = badgeText(item.badge);
|
|
17
|
+
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] }));
|
|
18
|
+
}
|
|
19
|
+
/** A link or a button, depending on what the item has. */
|
|
20
|
+
function ItemControl({ item, onNavigate }) {
|
|
21
|
+
const onClick = () => {
|
|
22
|
+
item.onClick?.();
|
|
23
|
+
onNavigate?.();
|
|
24
|
+
};
|
|
25
|
+
const children = _jsx(ItemContent, { item: item });
|
|
26
|
+
if (item.href !== undefined) {
|
|
27
|
+
const props = {
|
|
28
|
+
href: item.href,
|
|
29
|
+
className: 'ui-nav-item',
|
|
30
|
+
'aria-current': item.active ? 'page' : undefined,
|
|
31
|
+
onClick,
|
|
32
|
+
children,
|
|
33
|
+
};
|
|
34
|
+
return item.render ? item.render(props) : _jsx("a", { ...props });
|
|
35
|
+
}
|
|
36
|
+
return (_jsx("button", { type: "button", className: "ui-nav-item", "aria-current": item.active ? 'page' : undefined, onClick: onClick, children: children }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One entry in a navigation list: a link, a button, or a group of child links.
|
|
40
|
+
*
|
|
41
|
+
* Renders an `<li>`, so put it inside a `<ul>` (Sidebar does). Inside a collapsed Sidebar the
|
|
42
|
+
* label stays in the accessible name and is also shown as a tooltip, and a group opens its
|
|
43
|
+
* children as a menu beside the rail, since there is no room to expand it in place.
|
|
44
|
+
*/
|
|
45
|
+
export function NavItem(item) {
|
|
46
|
+
const { collapsed, onNavigate } = useNavContext();
|
|
47
|
+
const childActive = item.items?.some((c) => c.active) ?? false;
|
|
48
|
+
const [open, setOpen] = useState(item.defaultOpen ?? childActive);
|
|
49
|
+
const listId = useId();
|
|
50
|
+
if (!item.items) {
|
|
51
|
+
return (_jsx("li", { children: _jsx(Tooltip, { label: collapsed ? item.label : undefined, children: _jsx(ItemControl, { item: item, onNavigate: onNavigate }) }) }));
|
|
52
|
+
}
|
|
53
|
+
if (collapsed) {
|
|
54
|
+
return (_jsx("li", { children: _jsxs(ThemedDropdownMenu.Root, { children: [_jsx(ThemedDropdownMenu.Trigger, { children: _jsx("button", { type: "button", className: "ui-nav-item", "aria-current": childActive ? 'page' : undefined, children: _jsx(ItemContent, { item: item }) }) }), _jsxs(ThemedDropdownMenu.Content, { side: "right", align: "start", children: [_jsx(ThemedDropdownMenu.Label, { children: item.label }), item.items.map((child) => (_jsx(ThemedDropdownMenu.Item, { asChild: child.href !== undefined, onSelect: () => {
|
|
55
|
+
if (child.href === undefined) {
|
|
56
|
+
child.onClick?.();
|
|
57
|
+
onNavigate?.();
|
|
58
|
+
}
|
|
59
|
+
}, children: child.href !== undefined
|
|
60
|
+
? (child.render ?? ((p) => _jsx("a", { ...p })))({
|
|
61
|
+
href: child.href,
|
|
62
|
+
'aria-current': child.active ? 'page' : undefined,
|
|
63
|
+
onClick: () => {
|
|
64
|
+
child.onClick?.();
|
|
65
|
+
onNavigate?.();
|
|
66
|
+
},
|
|
67
|
+
children: child.label,
|
|
68
|
+
})
|
|
69
|
+
: child.label }, child.id ?? child.label)))] })] }) }));
|
|
70
|
+
}
|
|
71
|
+
return (_jsxs("li", { children: [_jsx("button", { type: "button", className: "ui-nav-item", "aria-expanded": open, "aria-controls": listId, onClick: () => setOpen((o) => !o), children: _jsx(ItemContent, { item: item, trailing: _jsx("span", { className: "ui-nav-item__chevron", "aria-hidden": "true", children: _jsx(ChevronDownIcon, {}) }) }) }), _jsx("ul", { id: listId, className: "ui-nav-group__items", hidden: !open, children: item.items.map((child) => (_jsx("li", { children: _jsx(ItemControl, { item: child, onNavigate: onNavigate }) }, child.id ?? child.label))) })] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|
|
13
|
+
export declare const NavContext: import("react").Context<NavContextValue>;
|
|
14
|
+
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;
|