@kennycorrea/ui 0.11.4 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -0
- package/README.md +251 -1
- package/dist/atoms/ActionMenu/ActionMenu.d.ts +37 -0
- package/dist/atoms/ActionMenu/ActionMenu.js +28 -0
- package/dist/atoms/ActionMenu/index.d.ts +1 -0
- package/dist/atoms/ActionMenu/index.js +1 -0
- package/dist/atoms/ActionSheet/ActionSheet.d.ts +10 -4
- package/dist/atoms/ActionSheet/ActionSheet.js +6 -15
- package/dist/atoms/ActionSheet/confirm.d.ts +1 -1
- package/dist/atoms/ActionSheet/confirm.js +1 -1
- package/dist/atoms/Avatar/Avatar.d.ts +1 -1
- package/dist/atoms/Badge/Badge.js +4 -4
- package/dist/atoms/Button/Button.js +2 -3
- package/dist/atoms/Button/Button.types.d.ts +5 -8
- package/dist/atoms/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/atoms/ContextMenu/ContextMenu.js +1 -1
- package/dist/atoms/Icon/Icon.d.ts +7 -9
- package/dist/atoms/Icon/Icon.js +23 -8
- package/dist/atoms/Pill/Pill.d.ts +17 -0
- package/dist/atoms/Pill/Pill.js +12 -0
- package/dist/atoms/Pill/index.d.ts +1 -0
- package/dist/atoms/Pill/index.js +1 -0
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.d.ts +9 -2
- package/dist/atoms/ResponsiveDialog/ResponsiveDialog.js +6 -15
- package/dist/atoms/StatusIndicator/StatusIndicator.d.ts +14 -0
- package/dist/atoms/StatusIndicator/StatusIndicator.js +8 -0
- package/dist/atoms/StatusIndicator/index.d.ts +1 -0
- package/dist/atoms/StatusIndicator/index.js +1 -0
- package/dist/atoms/TextLink/TextLink.d.ts +36 -0
- package/dist/atoms/TextLink/TextLink.js +17 -0
- package/dist/atoms/TextLink/index.d.ts +1 -0
- package/dist/atoms/TextLink/index.js +1 -0
- package/dist/atoms/Toast/Toast.d.ts +1 -1
- package/dist/atoms/Toast/Toast.js +1 -1
- package/dist/atoms/index.d.ts +4 -0
- package/dist/atoms/index.js +4 -0
- package/dist/hooks/breakpoints.d.ts +20 -0
- package/dist/hooks/breakpoints.js +13 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/useControllableState.d.ts +17 -0
- package/dist/hooks/useControllableState.js +22 -0
- package/dist/hooks/useElementWidth.d.ts +7 -0
- package/dist/hooks/useElementWidth.js +22 -0
- package/dist/hooks/useMediaQuery.d.ts +18 -0
- package/dist/hooks/useMediaQuery.js +51 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/molecules/AccentPicker/AccentPicker.d.ts +30 -0
- package/dist/molecules/AccentPicker/AccentPicker.js +27 -0
- package/dist/molecules/AccentPicker/index.d.ts +1 -0
- package/dist/molecules/AccentPicker/index.js +1 -0
- package/dist/molecules/Breadcrumbs/Breadcrumbs.d.ts +27 -0
- package/dist/molecules/Breadcrumbs/Breadcrumbs.js +40 -0
- package/dist/molecules/Breadcrumbs/index.d.ts +1 -0
- package/dist/molecules/Breadcrumbs/index.js +1 -0
- package/dist/molecules/Calendar/Calendar.d.ts +42 -0
- package/dist/molecules/Calendar/Calendar.js +111 -0
- package/dist/molecules/Calendar/index.d.ts +1 -0
- package/dist/molecules/Calendar/index.js +1 -0
- package/dist/molecules/DateInput/DateInput.d.ts +35 -0
- package/dist/molecules/DateInput/DateInput.js +41 -0
- package/dist/molecules/DateInput/index.d.ts +1 -0
- package/dist/molecules/DateInput/index.js +1 -0
- package/dist/molecules/EmptyState/EmptyState.d.ts +19 -0
- package/dist/molecules/EmptyState/EmptyState.js +7 -0
- package/dist/molecules/EmptyState/index.d.ts +1 -0
- package/dist/molecules/EmptyState/index.js +1 -0
- package/dist/molecules/ErrorState/ErrorState.d.ts +18 -0
- package/dist/molecules/ErrorState/ErrorState.js +12 -0
- package/dist/molecules/ErrorState/index.d.ts +1 -0
- package/dist/molecules/ErrorState/index.js +1 -0
- package/dist/molecules/ErrorSummary/ErrorSummary.d.ts +25 -0
- package/dist/molecules/ErrorSummary/ErrorSummary.js +32 -0
- package/dist/molecules/ErrorSummary/index.d.ts +1 -0
- package/dist/molecules/ErrorSummary/index.js +1 -0
- package/dist/molecules/FilterChips/FilterChips.d.ts +10 -0
- package/dist/molecules/FilterChips/FilterChips.js +13 -0
- package/dist/molecules/FilterChips/index.d.ts +1 -0
- package/dist/molecules/FilterChips/index.js +1 -0
- package/dist/molecules/FilterPopover/FilterPopover.d.ts +27 -0
- package/dist/molecules/FilterPopover/FilterPopover.js +32 -0
- package/dist/molecules/FilterPopover/index.d.ts +1 -0
- package/dist/molecules/FilterPopover/index.js +1 -0
- package/dist/molecules/FormActions/FormActions.d.ts +16 -0
- package/dist/molecules/FormActions/FormActions.js +5 -0
- package/dist/molecules/FormActions/index.d.ts +1 -0
- package/dist/molecules/FormActions/index.js +1 -0
- package/dist/molecules/FormField/FormField.d.ts +52 -0
- package/dist/molecules/FormField/FormField.js +63 -0
- package/dist/molecules/FormField/index.d.ts +1 -0
- package/dist/molecules/FormField/index.js +1 -0
- package/dist/molecules/FormSection/FormSection.d.ts +14 -0
- package/dist/molecules/FormSection/FormSection.js +10 -0
- package/dist/molecules/FormSection/index.d.ts +1 -0
- package/dist/molecules/FormSection/index.js +1 -0
- package/dist/molecules/KeyValueList/KeyValueList.d.ts +22 -0
- package/dist/molecules/KeyValueList/KeyValueList.js +9 -0
- package/dist/molecules/KeyValueList/index.d.ts +1 -0
- package/dist/molecules/KeyValueList/index.js +1 -0
- package/dist/molecules/LoadingState/LoadingState.d.ts +20 -0
- package/dist/molecules/LoadingState/LoadingState.js +22 -0
- package/dist/molecules/LoadingState/index.d.ts +1 -0
- package/dist/molecules/LoadingState/index.js +1 -0
- package/dist/molecules/NavItem/NavItem.d.ts +31 -0
- package/dist/molecules/NavItem/NavItem.js +72 -0
- package/dist/molecules/NavItem/context.d.ts +14 -0
- package/dist/molecules/NavItem/context.js +5 -0
- package/dist/molecules/NavItem/index.d.ts +1 -0
- package/dist/molecules/NavItem/index.js +1 -0
- package/dist/molecules/Pagination/Pagination.d.ts +32 -0
- package/dist/molecules/Pagination/Pagination.js +57 -0
- package/dist/molecules/Pagination/index.d.ts +1 -0
- package/dist/molecules/Pagination/index.js +1 -0
- package/dist/molecules/PasswordInput/PasswordInput.d.ts +14 -0
- package/dist/molecules/PasswordInput/PasswordInput.js +16 -0
- package/dist/molecules/PasswordInput/index.d.ts +1 -0
- package/dist/molecules/PasswordInput/index.js +1 -0
- package/dist/molecules/SearchField/SearchField.d.ts +24 -0
- package/dist/molecules/SearchField/SearchField.js +58 -0
- package/dist/molecules/SearchField/index.d.ts +1 -0
- package/dist/molecules/SearchField/index.js +1 -0
- package/dist/molecules/StatGroup/StatGroup.d.ts +21 -0
- package/dist/molecules/StatGroup/StatGroup.js +10 -0
- package/dist/molecules/StatGroup/index.d.ts +1 -0
- package/dist/molecules/StatGroup/index.js +1 -0
- package/dist/molecules/ThemeToggle/ThemeToggle.d.ts +23 -0
- package/dist/molecules/ThemeToggle/ThemeToggle.js +26 -0
- package/dist/molecules/ThemeToggle/index.d.ts +1 -0
- package/dist/molecules/ThemeToggle/index.js +1 -0
- package/dist/molecules/UserMenu/UserMenu.d.ts +20 -0
- package/dist/molecules/UserMenu/UserMenu.js +14 -0
- package/dist/molecules/UserMenu/index.d.ts +1 -0
- package/dist/molecules/UserMenu/index.js +1 -0
- package/dist/molecules/index.d.ts +21 -0
- package/dist/molecules/index.js +21 -0
- package/dist/organisms/AppShell/AppShell.d.ts +29 -0
- package/dist/organisms/AppShell/AppShell.js +34 -0
- package/dist/organisms/AppShell/context.d.ts +14 -0
- package/dist/organisms/AppShell/context.js +6 -0
- package/dist/organisms/AppShell/index.d.ts +1 -0
- package/dist/organisms/AppShell/index.js +1 -0
- package/dist/organisms/AuthForms/AuthForms.d.ts +83 -0
- package/dist/organisms/AuthForms/AuthForms.js +57 -0
- package/dist/organisms/AuthForms/index.d.ts +2 -0
- package/dist/organisms/AuthForms/index.js +1 -0
- package/dist/organisms/AuthForms/shared.d.ts +36 -0
- package/dist/organisms/AuthForms/shared.js +36 -0
- package/dist/organisms/BulkActionBar/BulkActionBar.d.ts +25 -0
- package/dist/organisms/BulkActionBar/BulkActionBar.js +18 -0
- package/dist/organisms/BulkActionBar/index.d.ts +1 -0
- package/dist/organisms/BulkActionBar/index.js +1 -0
- package/dist/organisms/Chart/CategoryCharts.d.ts +34 -0
- package/dist/organisms/Chart/CategoryCharts.js +93 -0
- package/dist/organisms/Chart/ChartFrame.d.ts +26 -0
- package/dist/organisms/Chart/ChartFrame.js +18 -0
- package/dist/organisms/Chart/DonutChart.d.ts +25 -0
- package/dist/organisms/Chart/DonutChart.js +53 -0
- package/dist/organisms/Chart/Sparkline.d.ts +15 -0
- package/dist/organisms/Chart/Sparkline.js +18 -0
- package/dist/organisms/Chart/index.d.ts +5 -0
- package/dist/organisms/Chart/index.js +5 -0
- package/dist/organisms/Chart/shared.d.ts +51 -0
- package/dist/organisms/Chart/shared.js +113 -0
- package/dist/organisms/DataTable/DataTable.d.ts +63 -0
- package/dist/organisms/DataTable/DataTable.js +119 -0
- package/dist/organisms/DataTable/index.d.ts +3 -0
- package/dist/organisms/DataTable/index.js +3 -0
- package/dist/organisms/DataTable/types.d.ts +39 -0
- package/dist/organisms/DataTable/types.js +1 -0
- package/dist/organisms/DataTable/useClientTable.d.ts +27 -0
- package/dist/organisms/DataTable/useClientTable.js +51 -0
- package/dist/organisms/DateRangePicker/DateRangePicker.d.ts +43 -0
- package/dist/organisms/DateRangePicker/DateRangePicker.js +54 -0
- package/dist/organisms/DateRangePicker/index.d.ts +1 -0
- package/dist/organisms/DateRangePicker/index.js +1 -0
- package/dist/organisms/FilterBar/FilterBar.d.ts +28 -0
- package/dist/organisms/FilterBar/FilterBar.js +16 -0
- package/dist/organisms/FilterBar/index.d.ts +1 -0
- package/dist/organisms/FilterBar/index.js +1 -0
- package/dist/organisms/PageHeader/PageHeader.d.ts +31 -0
- package/dist/organisms/PageHeader/PageHeader.js +19 -0
- package/dist/organisms/PageHeader/index.d.ts +1 -0
- package/dist/organisms/PageHeader/index.js +1 -0
- package/dist/organisms/PageTabs/PageTabs.d.ts +27 -0
- package/dist/organisms/PageTabs/PageTabs.js +31 -0
- package/dist/organisms/PageTabs/index.d.ts +1 -0
- package/dist/organisms/PageTabs/index.js +1 -0
- package/dist/organisms/Sidebar/Sidebar.d.ts +48 -0
- package/dist/organisms/Sidebar/Sidebar.js +61 -0
- package/dist/organisms/Sidebar/index.d.ts +1 -0
- package/dist/organisms/Sidebar/index.js +1 -0
- package/dist/organisms/Topbar/Topbar.d.ts +24 -0
- package/dist/organisms/Topbar/Topbar.js +18 -0
- package/dist/organisms/Topbar/index.d.ts +1 -0
- package/dist/organisms/Topbar/index.js +1 -0
- package/dist/organisms/index.d.ts +11 -0
- package/dist/organisms/index.js +11 -0
- package/dist/strings.d.ts +108 -15
- package/dist/strings.js +73 -7
- package/dist/templates/AdminLayout/AdminLayout.d.ts +14 -0
- package/dist/templates/AdminLayout/AdminLayout.js +9 -0
- package/dist/templates/AdminLayout/index.d.ts +1 -0
- package/dist/templates/AdminLayout/index.js +1 -0
- package/dist/templates/AuthLayout/AuthLayout.d.ts +28 -0
- package/dist/templates/AuthLayout/AuthLayout.js +11 -0
- package/dist/templates/AuthLayout/index.d.ts +1 -0
- package/dist/templates/AuthLayout/index.js +1 -0
- package/dist/templates/DashboardPage/DashboardPage.d.ts +25 -0
- package/dist/templates/DashboardPage/DashboardPage.js +12 -0
- package/dist/templates/DashboardPage/index.d.ts +1 -0
- package/dist/templates/DashboardPage/index.js +1 -0
- package/dist/templates/DetailPage/DetailPage.d.ts +20 -0
- package/dist/templates/DetailPage/DetailPage.js +11 -0
- package/dist/templates/DetailPage/index.d.ts +1 -0
- package/dist/templates/DetailPage/index.js +1 -0
- package/dist/templates/FormPage/FormPage.d.ts +26 -0
- package/dist/templates/FormPage/FormPage.js +11 -0
- package/dist/templates/FormPage/index.d.ts +1 -0
- package/dist/templates/FormPage/index.js +1 -0
- package/dist/templates/ListPage/ListPage.d.ts +17 -0
- package/dist/templates/ListPage/ListPage.js +8 -0
- package/dist/templates/ListPage/index.d.ts +1 -0
- package/dist/templates/ListPage/index.js +1 -0
- package/dist/templates/SettingsPage/SettingsPage.d.ts +32 -0
- package/dist/templates/SettingsPage/SettingsPage.js +13 -0
- package/dist/templates/SettingsPage/index.d.ts +1 -0
- package/dist/templates/SettingsPage/index.js +1 -0
- package/dist/templates/StatusPage/StatusPage.d.ts +18 -0
- package/dist/templates/StatusPage/StatusPage.js +8 -0
- package/dist/templates/StatusPage/index.d.ts +1 -0
- package/dist/templates/StatusPage/index.js +1 -0
- package/dist/templates/index.d.ts +8 -0
- package/dist/templates/index.js +8 -0
- package/dist/utils/dates.d.ts +67 -0
- package/dist/utils/dates.js +154 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/theme.d.ts +4 -0
- package/dist/utils/theme.js +7 -3
- package/package.json +4 -1
- package/tokens.css +494 -21
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Cross2Icon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
|
3
|
+
import { IconButton, Kbd, TextField } from '@radix-ui/themes';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { Input } from '../../atoms/Input/Input.js';
|
|
6
|
+
import { useUiStrings } from '../../strings.js';
|
|
7
|
+
function isEditable(el) {
|
|
8
|
+
if (!(el instanceof HTMLElement))
|
|
9
|
+
return false;
|
|
10
|
+
return el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A search input with a magnifier, a clear button once there is text, Escape to clear, and an
|
|
14
|
+
* optional page-wide shortcut key. The shortcut hint is hidden on touch screens, which have no
|
|
15
|
+
* keyboard to press it on, and once the field has text.
|
|
16
|
+
*/
|
|
17
|
+
export function SearchField({ value, defaultValue = '', onValueChange, debounceMs = 0, placeholder, 'aria-label': ariaLabel, shortcut, size, id, className, }) {
|
|
18
|
+
const strings = useUiStrings();
|
|
19
|
+
const [text, setText] = useState(value ?? defaultValue);
|
|
20
|
+
const inputRef = useRef(null);
|
|
21
|
+
const timer = useRef(undefined);
|
|
22
|
+
const report = useRef(onValueChange);
|
|
23
|
+
report.current = onValueChange;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (value !== undefined)
|
|
26
|
+
setText(value);
|
|
27
|
+
}, [value]);
|
|
28
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!shortcut)
|
|
31
|
+
return;
|
|
32
|
+
const onKey = (e) => {
|
|
33
|
+
if (e.key !== shortcut || e.metaKey || e.ctrlKey || e.altKey || isEditable(e.target))
|
|
34
|
+
return;
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
inputRef.current?.focus();
|
|
37
|
+
};
|
|
38
|
+
document.addEventListener('keydown', onKey);
|
|
39
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
40
|
+
}, [shortcut]);
|
|
41
|
+
const change = (next, immediate = false) => {
|
|
42
|
+
setText(next);
|
|
43
|
+
clearTimeout(timer.current);
|
|
44
|
+
if (immediate || debounceMs <= 0)
|
|
45
|
+
report.current(next);
|
|
46
|
+
else
|
|
47
|
+
timer.current = setTimeout(() => report.current(next), debounceMs);
|
|
48
|
+
};
|
|
49
|
+
return (_jsxs(Input, { ref: inputRef, id: id, type: "search", size: size, value: text, placeholder: placeholder, "aria-label": ariaLabel ?? strings.search, "aria-keyshortcuts": shortcut, className: className, onChange: (e) => change(e.target.value), onKeyDown: (e) => {
|
|
50
|
+
if (e.key === 'Escape' && text) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
change('', true);
|
|
53
|
+
}
|
|
54
|
+
}, children: [_jsx(TextField.Slot, { children: _jsx(MagnifyingGlassIcon, { "aria-hidden": "true" }) }), _jsx(TextField.Slot, { children: text ? (_jsx(IconButton, { type: "button", size: "1", variant: "ghost", color: "gray", "aria-label": strings.clearSearch, onClick: () => {
|
|
55
|
+
change('', true);
|
|
56
|
+
inputRef.current?.focus();
|
|
57
|
+
}, children: _jsx(Cross2Icon, {}) })) : shortcut ? (_jsx(Kbd, { className: "ui-search__kbd", "aria-hidden": "true", children: shortcut })) : null })] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SearchField.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './SearchField.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ScrollStripBreakpoint } from '../../atoms/ScrollStrip/ScrollStrip.js';
|
|
2
|
+
import { type StatTileProps } from '../../atoms/StatTile/StatTile.js';
|
|
3
|
+
export interface StatGroupItem extends StatTileProps {
|
|
4
|
+
/** React key. Defaults to the label when it is a string, else the index. */
|
|
5
|
+
id?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface StatGroupProps {
|
|
8
|
+
items: StatGroupItem[];
|
|
9
|
+
/** Grid columns from `gridFrom`. Default: the number of items, at most 4. */
|
|
10
|
+
columns?: number;
|
|
11
|
+
/** Below this the tiles scroll sideways; from it they are a grid. Default `md`. */
|
|
12
|
+
gridFrom?: ScrollStripBreakpoint;
|
|
13
|
+
/** Names the scrollable region (otherwise the `scrollableRegion` UI string). */
|
|
14
|
+
'aria-label'?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A row of KPI tiles: a swipeable strip on a phone, a grid from `gridFrom`. A `ScrollStrip` of
|
|
19
|
+
* `StatTile`s, so both keep their own behaviour and accessibility.
|
|
20
|
+
*/
|
|
21
|
+
export declare function StatGroup({ items, columns, gridFrom, 'aria-label': ariaLabel, className, }: StatGroupProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ScrollStrip } from '../../atoms/ScrollStrip/ScrollStrip.js';
|
|
3
|
+
import { StatTile } from '../../atoms/StatTile/StatTile.js';
|
|
4
|
+
/**
|
|
5
|
+
* A row of KPI tiles: a swipeable strip on a phone, a grid from `gridFrom`. A `ScrollStrip` of
|
|
6
|
+
* `StatTile`s, so both keep their own behaviour and accessibility.
|
|
7
|
+
*/
|
|
8
|
+
export function StatGroup({ items, columns, gridFrom = 'md', 'aria-label': ariaLabel, className, }) {
|
|
9
|
+
return (_jsx(ScrollStrip, { gridFrom: gridFrom, columns: columns ?? Math.min(items.length, 4), "aria-label": ariaLabel, className: className, children: items.map(({ id, ...tile }, i) => (_jsx(StatTile, { ...tile }, id ?? (typeof tile.label === 'string' ? tile.label : `stat-${i}`)))) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './StatGroup.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './StatGroup.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Theme } from '../../utils/theme.js';
|
|
2
|
+
/** The part of a `createTheming` instance the toggle uses. */
|
|
3
|
+
export interface ThemeStore {
|
|
4
|
+
readTheme(): Theme;
|
|
5
|
+
storeTheme(theme: Theme): void;
|
|
6
|
+
}
|
|
7
|
+
export interface ThemeToggleProps {
|
|
8
|
+
value?: Theme;
|
|
9
|
+
/** Initial value when uncontrolled. Default: `theming.readTheme()`, else `system`. */
|
|
10
|
+
defaultValue?: Theme;
|
|
11
|
+
onValueChange?: (theme: Theme) => void;
|
|
12
|
+
/** A `createTheming` instance: reads the stored choice and stores each new one. */
|
|
13
|
+
theming?: ThemeStore;
|
|
14
|
+
size?: 'sm' | 'md';
|
|
15
|
+
fullWidth?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* System / Light / Dark, as a segmented control.
|
|
19
|
+
*
|
|
20
|
+
* It chooses and remembers; it does not apply. Pass the value on to Radix's `<Theme
|
|
21
|
+
* appearance>` (or `applyTheme`) in `onValueChange`, because the app owns its `<Theme>`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ThemeToggle({ value, defaultValue, onValueChange, theming, size, fullWidth, }: ThemeToggleProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { SegmentedControl } from '../../atoms/SegmentedControl/SegmentedControl.js';
|
|
3
|
+
import { useControllableState } from '../../hooks/useControllableState.js';
|
|
4
|
+
import { useUiStrings } from '../../strings.js';
|
|
5
|
+
/**
|
|
6
|
+
* System / Light / Dark, as a segmented control.
|
|
7
|
+
*
|
|
8
|
+
* It chooses and remembers; it does not apply. Pass the value on to Radix's `<Theme
|
|
9
|
+
* appearance>` (or `applyTheme`) in `onValueChange`, because the app owns its `<Theme>`.
|
|
10
|
+
*/
|
|
11
|
+
export function ThemeToggle({ value, defaultValue, onValueChange, theming, size, fullWidth, }) {
|
|
12
|
+
const strings = useUiStrings();
|
|
13
|
+
const [current, setCurrent] = useControllableState({
|
|
14
|
+
value,
|
|
15
|
+
defaultValue: defaultValue ?? theming?.readTheme() ?? 'system',
|
|
16
|
+
onChange: (next) => {
|
|
17
|
+
theming?.storeTheme(next);
|
|
18
|
+
onValueChange?.(next);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
return (_jsx(SegmentedControl, { "aria-label": strings.appearance, size: size, fullWidth: fullWidth, value: current, onValueChange: (v) => setCurrent(v), options: [
|
|
22
|
+
{ value: 'system', label: strings.system },
|
|
23
|
+
{ value: 'light', label: strings.light },
|
|
24
|
+
{ value: 'dark', label: strings.dark },
|
|
25
|
+
] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ThemeToggle.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ThemeToggle.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ActionMenuItem } from '../../atoms/ActionMenu/ActionMenu.js';
|
|
2
|
+
import type { Breakpoint } from '../../hooks/breakpoints.js';
|
|
3
|
+
export interface UserMenuProps {
|
|
4
|
+
name: string;
|
|
5
|
+
/** Shown under the name in the sheet heading on small screens. */
|
|
6
|
+
email?: string;
|
|
7
|
+
avatarSrc?: string;
|
|
8
|
+
/** Profile, settings, sign out… Same shape as `ActionMenu`'s items. */
|
|
9
|
+
items: ActionMenuItem[];
|
|
10
|
+
onSelect: (id: string) => void;
|
|
11
|
+
/** Passed to ActionMenu. Default `sm`. */
|
|
12
|
+
sheetBelow?: Breakpoint | false;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The signed-in person's avatar and name, opening their account actions.
|
|
16
|
+
*
|
|
17
|
+
* Below 768px the name is visually hidden (still read out) and only the avatar shows, and the
|
|
18
|
+
* menu becomes a sheet headed by the name and email.
|
|
19
|
+
*/
|
|
20
|
+
export declare function UserMenu({ name, email, avatarSrc, items, onSelect, sheetBelow }: UserMenuProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ActionMenu } from '../../atoms/ActionMenu/ActionMenu.js';
|
|
3
|
+
import { Avatar } from '../../atoms/Avatar/Avatar.js';
|
|
4
|
+
import { useUiStrings } from '../../strings.js';
|
|
5
|
+
/**
|
|
6
|
+
* The signed-in person's avatar and name, opening their account actions.
|
|
7
|
+
*
|
|
8
|
+
* Below 768px the name is visually hidden (still read out) and only the avatar shows, and the
|
|
9
|
+
* menu becomes a sheet headed by the name and email.
|
|
10
|
+
*/
|
|
11
|
+
export function UserMenu({ name, email, avatarSrc, items, onSelect, sheetBelow }) {
|
|
12
|
+
const strings = useUiStrings();
|
|
13
|
+
return (_jsx(ActionMenu, { items: items, onSelect: onSelect, sheetBelow: sheetBelow, title: _jsxs(_Fragment, { children: [name, email ? _jsx("span", { className: "block text-sm font-normal text-ink-muted", children: email }) : null] }), trigger: _jsxs("button", { type: "button", className: "ui-user-menu__trigger", children: [_jsx("span", { "aria-hidden": "true", className: "inline-flex", children: _jsx(Avatar, { src: avatarSrc, name: name }) }), _jsx("span", { className: "ui-user-menu__name", children: name }), _jsxs("span", { className: "ui-sr-only", children: [", ", strings.userMenu] })] }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './UserMenu.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './UserMenu.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './AccentPicker/index.js';
|
|
2
|
+
export * from './Breadcrumbs/index.js';
|
|
3
|
+
export * from './Calendar/index.js';
|
|
4
|
+
export * from './DateInput/index.js';
|
|
5
|
+
export * from './EmptyState/index.js';
|
|
6
|
+
export * from './ErrorState/index.js';
|
|
7
|
+
export * from './ErrorSummary/index.js';
|
|
8
|
+
export * from './FilterChips/index.js';
|
|
9
|
+
export * from './FilterPopover/index.js';
|
|
10
|
+
export * from './FormActions/index.js';
|
|
11
|
+
export * from './FormField/index.js';
|
|
12
|
+
export * from './FormSection/index.js';
|
|
13
|
+
export * from './KeyValueList/index.js';
|
|
14
|
+
export * from './LoadingState/index.js';
|
|
15
|
+
export * from './NavItem/index.js';
|
|
16
|
+
export * from './Pagination/index.js';
|
|
17
|
+
export * from './PasswordInput/index.js';
|
|
18
|
+
export * from './SearchField/index.js';
|
|
19
|
+
export * from './StatGroup/index.js';
|
|
20
|
+
export * from './ThemeToggle/index.js';
|
|
21
|
+
export * from './UserMenu/index.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './AccentPicker/index.js';
|
|
2
|
+
export * from './Breadcrumbs/index.js';
|
|
3
|
+
export * from './Calendar/index.js';
|
|
4
|
+
export * from './DateInput/index.js';
|
|
5
|
+
export * from './EmptyState/index.js';
|
|
6
|
+
export * from './ErrorState/index.js';
|
|
7
|
+
export * from './ErrorSummary/index.js';
|
|
8
|
+
export * from './FilterChips/index.js';
|
|
9
|
+
export * from './FilterPopover/index.js';
|
|
10
|
+
export * from './FormActions/index.js';
|
|
11
|
+
export * from './FormField/index.js';
|
|
12
|
+
export * from './FormSection/index.js';
|
|
13
|
+
export * from './KeyValueList/index.js';
|
|
14
|
+
export * from './LoadingState/index.js';
|
|
15
|
+
export * from './NavItem/index.js';
|
|
16
|
+
export * from './Pagination/index.js';
|
|
17
|
+
export * from './PasswordInput/index.js';
|
|
18
|
+
export * from './SearchField/index.js';
|
|
19
|
+
export * from './StatGroup/index.js';
|
|
20
|
+
export * from './ThemeToggle/index.js';
|
|
21
|
+
export * from './UserMenu/index.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type TabBarProps } from '../../atoms/TabBar/TabBar.js';
|
|
3
|
+
import { type Breakpoint } from '../../hooks/breakpoints.js';
|
|
4
|
+
export interface AppShellProps {
|
|
5
|
+
/** A `Sidebar`. */
|
|
6
|
+
sidebar: ReactNode;
|
|
7
|
+
/** A `Topbar`, above the content. */
|
|
8
|
+
topbar?: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* A bottom TabBar for small screens, hidden from `breakpoint` up. Optional: the off-canvas
|
|
11
|
+
* sidebar is always available through the Topbar's menu button.
|
|
12
|
+
*/
|
|
13
|
+
mobileNav?: Omit<TabBarProps, 'hideFrom'>;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/** From here up the sidebar is a persistent column. Default `md` (1024px). */
|
|
16
|
+
breakpoint?: Breakpoint;
|
|
17
|
+
/** Controlled open state of the off-canvas sidebar. */
|
|
18
|
+
navOpen?: boolean;
|
|
19
|
+
onNavOpenChange?: (open: boolean) => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The admin frame: sidebar, topbar and content, with a skip link to the content.
|
|
24
|
+
*
|
|
25
|
+
* Wide screens get a two-column grid with a sticky sidebar. Narrow ones get one column, the
|
|
26
|
+
* sidebar off-canvas behind the Topbar's menu button, and optionally a bottom TabBar. The
|
|
27
|
+
* shell is at least the dynamic viewport's height, so the browser's own bars do not cut it off.
|
|
28
|
+
*/
|
|
29
|
+
export declare function AppShell({ sidebar, topbar, mobileNav, children, breakpoint, navOpen: navOpenProp, onNavOpenChange, className, }: AppShellProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useState } from 'react';
|
|
3
|
+
import { TabBar, TabBarSpacer } from '../../atoms/TabBar/TabBar.js';
|
|
4
|
+
import { minWidth } from '../../hooks/breakpoints.js';
|
|
5
|
+
import { useMediaQuery } from '../../hooks/useMediaQuery.js';
|
|
6
|
+
import { useUiStrings } from '../../strings.js';
|
|
7
|
+
import { ShellContext } from './context.js';
|
|
8
|
+
/**
|
|
9
|
+
* The admin frame: sidebar, topbar and content, with a skip link to the content.
|
|
10
|
+
*
|
|
11
|
+
* Wide screens get a two-column grid with a sticky sidebar. Narrow ones get one column, the
|
|
12
|
+
* sidebar off-canvas behind the Topbar's menu button, and optionally a bottom TabBar. The
|
|
13
|
+
* shell is at least the dynamic viewport's height, so the browser's own bars do not cut it off.
|
|
14
|
+
*/
|
|
15
|
+
export function AppShell({ sidebar, topbar, mobileNav, children, breakpoint = 'md', navOpen: navOpenProp, onNavOpenChange, className, }) {
|
|
16
|
+
const strings = useUiStrings();
|
|
17
|
+
const wide = useMediaQuery(minWidth(breakpoint));
|
|
18
|
+
const [ownOpen, setOwnOpen] = useState(false);
|
|
19
|
+
const mainId = useId();
|
|
20
|
+
const navOpen = navOpenProp ?? ownOpen;
|
|
21
|
+
const setNavOpen = (open) => {
|
|
22
|
+
if (navOpenProp === undefined)
|
|
23
|
+
setOwnOpen(open);
|
|
24
|
+
onNavOpenChange?.(open);
|
|
25
|
+
};
|
|
26
|
+
// Widening the window with the panel open closes it for good, rather than leaving it open
|
|
27
|
+
// behind the persistent sidebar to reappear the next time the window narrows.
|
|
28
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: setNavOpen is rebuilt every render; only these two values decide.
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (wide && navOpen)
|
|
31
|
+
setNavOpen(false);
|
|
32
|
+
}, [wide, navOpen]);
|
|
33
|
+
return (_jsx(ShellContext.Provider, { value: { breakpoint, wide, navOpen: navOpen && !wide, setNavOpen }, children: _jsxs("div", { className: ['ui-shell', className].filter(Boolean).join(' '), "data-wide": wide ? '' : undefined, "data-tabbar": mobileNav && !wide ? '' : undefined, children: [_jsx("a", { className: "ui-skip-link", href: `#${mainId}`, children: strings.skipToContent }), sidebar, _jsxs("div", { className: "ui-shell__main", children: [topbar, _jsx("main", { id: mainId, tabIndex: -1, className: "ui-shell__content", children: children }), mobileNav ? (_jsxs(_Fragment, { children: [_jsx(TabBarSpacer, { hideFrom: breakpoint }), _jsx(TabBar, { ...mobileNav, hideFrom: breakpoint })] })) : null] })] }) }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Breakpoint } from '../../hooks/breakpoints.js';
|
|
2
|
+
/**
|
|
3
|
+
* What AppShell shares with the Sidebar and Topbar inside it: whether the screen is wide enough
|
|
4
|
+
* for a persistent sidebar, and the open state of the off-canvas one when it is not.
|
|
5
|
+
*/
|
|
6
|
+
export interface ShellContextValue {
|
|
7
|
+
breakpoint: Breakpoint;
|
|
8
|
+
wide: boolean;
|
|
9
|
+
navOpen: boolean;
|
|
10
|
+
setNavOpen: (open: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare const ShellContext: import("react").Context<ShellContextValue | null>;
|
|
13
|
+
/** `null` outside an AppShell; Sidebar and Topbar then manage themselves. */
|
|
14
|
+
export declare function useShell(): ShellContextValue | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AppShell.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AppShell.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type AuthFormBaseProps } from './shared.js';
|
|
3
|
+
export interface LoginValues {
|
|
4
|
+
identifier: string;
|
|
5
|
+
password: string;
|
|
6
|
+
remember: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface LoginFormProps extends AuthFormBaseProps<'identifier' | 'password'> {
|
|
9
|
+
onSubmit: (values: LoginValues) => void;
|
|
10
|
+
labels: {
|
|
11
|
+
/** Names the form. */
|
|
12
|
+
form: string;
|
|
13
|
+
identifier: string;
|
|
14
|
+
password: string;
|
|
15
|
+
submit: string;
|
|
16
|
+
/** With it, a "remember me" checkbox. */
|
|
17
|
+
remember?: string;
|
|
18
|
+
};
|
|
19
|
+
/** `email` (the default) or `username`. Both use `autocomplete="username"`. */
|
|
20
|
+
identifierType?: 'email' | 'username';
|
|
21
|
+
/** A "Forgot password?" link, shown under the password field. */
|
|
22
|
+
forgotPassword?: ReactNode;
|
|
23
|
+
defaultValues?: Partial<LoginValues>;
|
|
24
|
+
}
|
|
25
|
+
export declare function LoginForm({ onSubmit, labels, identifierType, forgotPassword, defaultValues, errors, formError, pending, footer, className, }: LoginFormProps): import("react").JSX.Element;
|
|
26
|
+
export interface SignupValues {
|
|
27
|
+
name: string;
|
|
28
|
+
email: string;
|
|
29
|
+
password: string;
|
|
30
|
+
acceptedTerms: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface SignupFormProps extends AuthFormBaseProps<'name' | 'email' | 'password' | 'acceptedTerms'> {
|
|
33
|
+
onSubmit: (values: SignupValues) => void;
|
|
34
|
+
labels: {
|
|
35
|
+
form: string;
|
|
36
|
+
/** With it, a name field. */
|
|
37
|
+
name?: string;
|
|
38
|
+
email: string;
|
|
39
|
+
password: string;
|
|
40
|
+
/** The rules, shown under the password field and linked to it. */
|
|
41
|
+
passwordHint?: string;
|
|
42
|
+
/** With it, a required checkbox; may contain links to the terms. */
|
|
43
|
+
terms?: ReactNode;
|
|
44
|
+
submit: string;
|
|
45
|
+
};
|
|
46
|
+
defaultValues?: Partial<SignupValues>;
|
|
47
|
+
}
|
|
48
|
+
export declare function SignupForm({ onSubmit, labels, defaultValues, errors, formError, pending, footer, className, }: SignupFormProps): import("react").JSX.Element;
|
|
49
|
+
export interface ForgotPasswordFormProps extends AuthFormBaseProps<'email'> {
|
|
50
|
+
onSubmit: (values: {
|
|
51
|
+
email: string;
|
|
52
|
+
}) => void;
|
|
53
|
+
labels: {
|
|
54
|
+
form: string;
|
|
55
|
+
email: string;
|
|
56
|
+
submit: string;
|
|
57
|
+
/** Shown once `sent`: "If that address has an account, a link is on its way." */
|
|
58
|
+
sent: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The request went out. Shows `labels.sent` as a status message and keeps the form, so the
|
|
62
|
+
* person can correct the address or ask again. Word `labels.sent` so it does not reveal
|
|
63
|
+
* whether the address has an account.
|
|
64
|
+
*/
|
|
65
|
+
sent?: boolean;
|
|
66
|
+
defaultEmail?: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function ForgotPasswordForm({ onSubmit, labels, sent, defaultEmail, errors, formError, pending, footer, className, }: ForgotPasswordFormProps): import("react").JSX.Element;
|
|
69
|
+
export interface ResetPasswordValues {
|
|
70
|
+
password: string;
|
|
71
|
+
confirmPassword: string;
|
|
72
|
+
}
|
|
73
|
+
export interface ResetPasswordFormProps extends AuthFormBaseProps<'password' | 'confirmPassword'> {
|
|
74
|
+
onSubmit: (values: ResetPasswordValues) => void;
|
|
75
|
+
labels: {
|
|
76
|
+
form: string;
|
|
77
|
+
password: string;
|
|
78
|
+
confirmPassword: string;
|
|
79
|
+
passwordHint?: string;
|
|
80
|
+
submit: string;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export declare function ResetPasswordForm({ onSubmit, labels, errors, formError, pending, footer, className, }: ResetPasswordFormProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Alert } from '../../atoms/Alert/Alert.js';
|
|
4
|
+
import { Checkbox } from '../../atoms/Checkbox/Checkbox.js';
|
|
5
|
+
import { Input } from '../../atoms/Input/Input.js';
|
|
6
|
+
import { FormField } from '../../molecules/FormField/FormField.js';
|
|
7
|
+
import { PasswordInput } from '../../molecules/PasswordInput/PasswordInput.js';
|
|
8
|
+
import { AuthFormShell, useAuthForm } from './shared.js';
|
|
9
|
+
const LOGIN_FIELDS = ['identifier', 'password'];
|
|
10
|
+
export function LoginForm({ onSubmit, labels, identifierType = 'email', forgotPassword, defaultValues, errors, formError, pending, footer, className, }) {
|
|
11
|
+
const { ids, markSubmitted } = useAuthForm(LOGIN_FIELDS, errors);
|
|
12
|
+
const [values, setValues] = useState({
|
|
13
|
+
identifier: '',
|
|
14
|
+
password: '',
|
|
15
|
+
remember: false,
|
|
16
|
+
...defaultValues,
|
|
17
|
+
});
|
|
18
|
+
const set = (k, v) => setValues((s) => ({ ...s, [k]: v }));
|
|
19
|
+
return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
|
|
20
|
+
markSubmitted();
|
|
21
|
+
onSubmit(values);
|
|
22
|
+
}, children: [_jsx(FormField, { id: ids.identifier, label: labels.identifier, required: true, error: errors?.identifier, children: _jsx(Input, { type: identifierType === 'email' ? 'email' : 'text', inputMode: identifierType === 'email' ? 'email' : undefined, autoComplete: "username", autoCapitalize: "none", spellCheck: false, value: values.identifier, onChange: (e) => set('identifier', e.target.value) }) }), _jsx(FormField, { id: ids.password, label: labels.password, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "current-password", value: values.password, onChange: (e) => set('password', e.target.value) }) }), forgotPassword || labels.remember ? (_jsxs("div", { className: "ui-auth-form__row", children: [labels.remember ? (_jsx(FormField, { label: labels.remember, layout: "inline", children: _jsx(Checkbox, { checked: values.remember, onCheckedChange: (c) => set('remember', c === true) }) })) : (_jsx("span", {})), forgotPassword] })) : null] }));
|
|
23
|
+
}
|
|
24
|
+
const SIGNUP_FIELDS = ['name', 'email', 'password', 'acceptedTerms'];
|
|
25
|
+
export function SignupForm({ onSubmit, labels, defaultValues, errors, formError, pending, footer, className, }) {
|
|
26
|
+
const { ids, markSubmitted } = useAuthForm(SIGNUP_FIELDS, errors);
|
|
27
|
+
const [values, setValues] = useState({
|
|
28
|
+
name: '',
|
|
29
|
+
email: '',
|
|
30
|
+
password: '',
|
|
31
|
+
acceptedTerms: false,
|
|
32
|
+
...defaultValues,
|
|
33
|
+
});
|
|
34
|
+
const set = (k, v) => setValues((s) => ({ ...s, [k]: v }));
|
|
35
|
+
return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
|
|
36
|
+
markSubmitted();
|
|
37
|
+
onSubmit(values);
|
|
38
|
+
}, children: [labels.name ? (_jsx(FormField, { id: ids.name, label: labels.name, required: true, error: errors?.name, children: _jsx(Input, { autoComplete: "name", value: values.name, onChange: (e) => set('name', e.target.value) }) })) : null, _jsx(FormField, { id: ids.email, label: labels.email, required: true, error: errors?.email, children: _jsx(Input, { type: "email", inputMode: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: false, value: values.email, onChange: (e) => set('email', e.target.value) }) }), _jsx(FormField, { id: ids.password, label: labels.password, hint: labels.passwordHint, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.password, onChange: (e) => set('password', e.target.value) }) }), labels.terms ? (_jsx(FormField, { id: ids.acceptedTerms, label: labels.terms, layout: "inline", required: true, error: errors?.acceptedTerms, children: _jsx(Checkbox, { checked: values.acceptedTerms, onCheckedChange: (c) => set('acceptedTerms', c === true) }) })) : null] }));
|
|
39
|
+
}
|
|
40
|
+
const FORGOT_FIELDS = ['email'];
|
|
41
|
+
export function ForgotPasswordForm({ onSubmit, labels, sent = false, defaultEmail = '', errors, formError, pending, footer, className, }) {
|
|
42
|
+
const { ids, markSubmitted } = useAuthForm(FORGOT_FIELDS, errors);
|
|
43
|
+
const [email, setEmail] = useState(defaultEmail);
|
|
44
|
+
return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
|
|
45
|
+
markSubmitted();
|
|
46
|
+
onSubmit({ email });
|
|
47
|
+
}, children: [sent ? _jsx(Alert, { type: "success", body: labels.sent }) : null, _jsx(FormField, { id: ids.email, label: labels.email, required: true, error: errors?.email, children: _jsx(Input, { type: "email", inputMode: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: false, value: email, onChange: (e) => setEmail(e.target.value) }) })] }));
|
|
48
|
+
}
|
|
49
|
+
const RESET_FIELDS = ['password', 'confirmPassword'];
|
|
50
|
+
export function ResetPasswordForm({ onSubmit, labels, errors, formError, pending, footer, className, }) {
|
|
51
|
+
const { ids, markSubmitted } = useAuthForm(RESET_FIELDS, errors);
|
|
52
|
+
const [values, setValues] = useState({ password: '', confirmPassword: '' });
|
|
53
|
+
return (_jsxs(AuthFormShell, { label: labels.form, submitLabel: labels.submit, formError: formError, pending: pending, footer: footer, className: className, onSubmit: () => {
|
|
54
|
+
markSubmitted();
|
|
55
|
+
onSubmit(values);
|
|
56
|
+
}, children: [_jsx(FormField, { id: ids.password, label: labels.password, hint: labels.passwordHint, required: true, error: errors?.password, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.password, onChange: (e) => setValues((v) => ({ ...v, password: e.target.value })) }) }), _jsx(FormField, { id: ids.confirmPassword, label: labels.confirmPassword, required: true, error: errors?.confirmPassword, children: _jsx(PasswordInput, { autoComplete: "new-password", value: values.confirmPassword, onChange: (e) => setValues((v) => ({ ...v, confirmPassword: e.target.value })) }) })] }));
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AuthForms.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type FormEvent, type ReactNode } from 'react';
|
|
2
|
+
/** What every auth form takes besides its own labels. */
|
|
3
|
+
export interface AuthFormBaseProps<Field extends string> {
|
|
4
|
+
/**
|
|
5
|
+
* Field messages from your validation or your server, by field. After a submit, focus moves
|
|
6
|
+
* to the first field that has one.
|
|
7
|
+
*/
|
|
8
|
+
errors?: Partial<Record<Field, string>>;
|
|
9
|
+
/** A message about the whole attempt ("Those details did not match"). Announced as an alert. */
|
|
10
|
+
formError?: string;
|
|
11
|
+
/** Disables the submit button and shows its spinner while a request is in flight. */
|
|
12
|
+
pending?: boolean;
|
|
13
|
+
/** Under the form: "No account? Sign up", legal links. */
|
|
14
|
+
footer?: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The ids for a form's fields, and focus on the first invalid one after each submit.
|
|
19
|
+
*
|
|
20
|
+
* Focus moves only in answer to a submit, never when `errors` changes for another reason, so a
|
|
21
|
+
* field validated as the person types does not pull the cursor away from where they are.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useAuthForm<Field extends string>(fields: readonly Field[], errors: Partial<Record<Field, string>> | undefined): {
|
|
24
|
+
ids: Record<Field, string>;
|
|
25
|
+
markSubmitted: () => void;
|
|
26
|
+
};
|
|
27
|
+
export declare function AuthFormShell({ label, onSubmit, formError, submitLabel, pending, footer, className, children, }: {
|
|
28
|
+
label: string;
|
|
29
|
+
onSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
|
30
|
+
formError?: string;
|
|
31
|
+
submitLabel: string;
|
|
32
|
+
pending?: boolean;
|
|
33
|
+
footer?: ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef } from 'react';
|
|
3
|
+
import { Alert } from '../../atoms/Alert/Alert.js';
|
|
4
|
+
import { Button } from '../../atoms/Button/Button.js';
|
|
5
|
+
/**
|
|
6
|
+
* The ids for a form's fields, and focus on the first invalid one after each submit.
|
|
7
|
+
*
|
|
8
|
+
* Focus moves only in answer to a submit, never when `errors` changes for another reason, so a
|
|
9
|
+
* field validated as the person types does not pull the cursor away from where they are.
|
|
10
|
+
*/
|
|
11
|
+
export function useAuthForm(fields, errors) {
|
|
12
|
+
const base = useId();
|
|
13
|
+
const ids = Object.fromEntries(fields.map((f) => [f, `${base}-${f}`]));
|
|
14
|
+
const submitted = useRef(false);
|
|
15
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: ids and fields are stable for the form's lifetime.
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!submitted.current || !errors)
|
|
18
|
+
return;
|
|
19
|
+
const first = fields.find((f) => errors[f]);
|
|
20
|
+
if (!first)
|
|
21
|
+
return;
|
|
22
|
+
submitted.current = false;
|
|
23
|
+
document.getElementById(ids[first])?.focus();
|
|
24
|
+
}, [errors]);
|
|
25
|
+
const markSubmitted = () => {
|
|
26
|
+
submitted.current = true;
|
|
27
|
+
};
|
|
28
|
+
return { ids, markSubmitted };
|
|
29
|
+
}
|
|
30
|
+
export function AuthFormShell({ label, onSubmit, formError, submitLabel, pending, footer, className, children, }) {
|
|
31
|
+
return (_jsxs("form", { noValidate: true, "aria-label": label, className: ['ui-auth-form', className].filter(Boolean).join(' '), onSubmit: (e) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
if (!pending)
|
|
34
|
+
onSubmit(e);
|
|
35
|
+
}, children: [formError ? _jsx(Alert, { type: "error", body: formError }) : null, children, _jsx(Button, { type: "submit", size: "touch", fullWidth: true, loading: pending, children: submitLabel }), footer ? _jsx("div", { className: "ui-auth-form__footer", children: footer }) : null] }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface BulkAction {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: ReactNode;
|
|
6
|
+
/** Red, for the one that deletes or cancels. */
|
|
7
|
+
destructive?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface BulkActionBarProps {
|
|
11
|
+
selectedCount: number;
|
|
12
|
+
actions: BulkAction[];
|
|
13
|
+
onAction: (id: string) => void;
|
|
14
|
+
onClear: () => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* What to do with the selected rows: a floating toolbar that appears while anything is
|
|
19
|
+
* selected and sticks to the bottom of the content, above the TabBar on small screens.
|
|
20
|
+
*
|
|
21
|
+
* A Radix `Toolbar`, so its buttons are one Tab stop with arrow keys between them. The count
|
|
22
|
+
* sits in a polite live region that stays in the page while the bar is hidden, so a screen
|
|
23
|
+
* reader hears the selection change.
|
|
24
|
+
*/
|
|
25
|
+
export declare function BulkActionBar({ selectedCount, actions, onAction, onClear, className, }: BulkActionBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Cross2Icon } from '@radix-ui/react-icons';
|
|
3
|
+
import * as Toolbar from '@radix-ui/react-toolbar';
|
|
4
|
+
import { Button as ThemedButton } from '@radix-ui/themes';
|
|
5
|
+
import { useUiStrings } from '../../strings.js';
|
|
6
|
+
/**
|
|
7
|
+
* What to do with the selected rows: a floating toolbar that appears while anything is
|
|
8
|
+
* selected and sticks to the bottom of the content, above the TabBar on small screens.
|
|
9
|
+
*
|
|
10
|
+
* A Radix `Toolbar`, so its buttons are one Tab stop with arrow keys between them. The count
|
|
11
|
+
* sits in a polite live region that stays in the page while the bar is hidden, so a screen
|
|
12
|
+
* reader hears the selection change.
|
|
13
|
+
*/
|
|
14
|
+
export function BulkActionBar({ selectedCount, actions, onAction, onClear, className, }) {
|
|
15
|
+
const strings = useUiStrings();
|
|
16
|
+
const count = strings.selectedCount(selectedCount);
|
|
17
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "ui-sr-only", "aria-live": "polite", children: selectedCount > 0 ? count : '' }), selectedCount > 0 ? (_jsxs(Toolbar.Root, { "aria-label": strings.bulkActions, className: ['ui-bulk-bar', className].filter(Boolean).join(' '), children: [_jsx("span", { className: "ui-bulk-bar__count", "aria-hidden": "true", children: count }), actions.map((action) => (_jsx(Toolbar.Button, { asChild: true, disabled: action.disabled, children: _jsxs(ThemedButton, { size: "2", variant: action.destructive ? 'solid' : 'soft', color: action.destructive ? 'red' : undefined, onClick: () => onAction(action.id), children: [action.icon, action.label] }) }, action.id))), _jsx(Toolbar.Button, { asChild: true, children: _jsxs(ThemedButton, { size: "2", variant: "ghost", color: "gray", onClick: onClear, children: [_jsx(Cross2Icon, { "aria-hidden": "true" }), strings.clearSelection] }) })] })) : null] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './BulkActionBar.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './BulkActionBar.js';
|