@ahrowe/ui 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -1
  3. package/dist/cjs-types/common/actionIcon/actionIcon.types.d.ts +10 -6
  4. package/dist/cjs-types/common/ahroweProvider/ahroweProvider.d.ts +1 -1
  5. package/dist/cjs-types/common/button/button.types.d.ts +25 -8
  6. package/dist/cjs-types/common/chatMessageList/chatMessageList.types.d.ts +1 -1
  7. package/dist/cjs-types/common/configProvider/configProvider.d.ts +2 -1
  8. package/dist/cjs-types/common/configProvider/configProvider.types.d.ts +8 -0
  9. package/dist/cjs-types/common/copyButton/copyButton.types.d.ts +4 -1
  10. package/dist/cjs-types/common/dropdown/dropdown.types.d.ts +3 -2
  11. package/dist/cjs-types/common/infiniteBlock/useInfiniteScroll.d.ts +1 -1
  12. package/dist/cjs-types/common/listItem/index.d.ts +2 -0
  13. package/dist/cjs-types/common/listItem/listItem.d.ts +4 -0
  14. package/dist/cjs-types/common/listItem/listItem.types.d.ts +33 -0
  15. package/dist/cjs-types/common/menu/menu.types.d.ts +2 -2
  16. package/dist/cjs-types/common/menu/menuItem.d.ts +5 -2
  17. package/dist/cjs-types/common/themeProvider/theme.types.d.ts +4 -0
  18. package/dist/cjs-types/common/utils/navigation.d.ts +12 -0
  19. package/dist/cjs-types/index.d.ts +2 -0
  20. package/dist/esm/common/ahroweProvider/ahroweProvider.mjs +1 -1
  21. package/dist/esm/common/ahroweProvider/ahroweProvider.mjs.map +1 -1
  22. package/dist/esm/common/breadcrumb/breadcrumb.mjs +1 -1
  23. package/dist/esm/common/breadcrumb/breadcrumb.mjs.map +1 -1
  24. package/dist/esm/common/button/button.mjs +1 -1
  25. package/dist/esm/common/button/button.mjs.map +1 -1
  26. package/dist/esm/common/button/button.module.mjs +1 -1
  27. package/dist/esm/common/button/button.module.mjs.map +1 -1
  28. package/dist/esm/common/button/button.types.mjs.map +1 -1
  29. package/dist/esm/common/chatMessageList/chatMessageList.mjs +1 -1
  30. package/dist/esm/common/chatMessageList/chatMessageList.mjs.map +1 -1
  31. package/dist/esm/common/commandPalette/commandPalette.mjs +1 -1
  32. package/dist/esm/common/commandPalette/commandPalette.mjs.map +1 -1
  33. package/dist/esm/common/configProvider/configProvider.mjs +1 -1
  34. package/dist/esm/common/configProvider/configProvider.mjs.map +1 -1
  35. package/dist/esm/common/dropdown/dropdown.mjs +1 -1
  36. package/dist/esm/common/dropdown/dropdown.mjs.map +1 -1
  37. package/dist/esm/common/dropdown/dropdown.module.mjs +1 -1
  38. package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
  39. package/dist/esm/common/infiniteBlock/useInfiniteScroll.mjs +1 -1
  40. package/dist/esm/common/infiniteBlock/useInfiniteScroll.mjs.map +1 -1
  41. package/dist/esm/common/listItem/listItem.mjs +2 -0
  42. package/dist/esm/common/listItem/listItem.mjs.map +1 -0
  43. package/dist/esm/common/listItem/listItem.module.mjs +2 -0
  44. package/dist/esm/common/listItem/listItem.module.mjs.map +1 -0
  45. package/dist/esm/common/loader/loader.module.mjs.map +1 -1
  46. package/dist/esm/common/menu/menu.mjs +1 -1
  47. package/dist/esm/common/menu/menu.mjs.map +1 -1
  48. package/dist/esm/common/menu/menu.types.mjs.map +1 -1
  49. package/dist/esm/common/menu/menuItem.mjs +1 -1
  50. package/dist/esm/common/menu/menuItem.mjs.map +1 -1
  51. package/dist/esm/common/utils/navigation.mjs +2 -0
  52. package/dist/esm/common/utils/navigation.mjs.map +1 -0
  53. package/dist/esm/index.mjs +1 -1
  54. package/dist/index.cjs +5 -5
  55. package/dist/index.cjs.map +1 -1
  56. package/dist/style.css +1 -1
  57. package/dist/types/common/actionIcon/actionIcon.types.d.ts +10 -6
  58. package/dist/types/common/ahroweProvider/ahroweProvider.d.ts +1 -1
  59. package/dist/types/common/button/button.types.d.ts +25 -8
  60. package/dist/types/common/chatMessageList/chatMessageList.types.d.ts +1 -1
  61. package/dist/types/common/configProvider/configProvider.d.ts +2 -1
  62. package/dist/types/common/configProvider/configProvider.types.d.ts +8 -0
  63. package/dist/types/common/copyButton/copyButton.types.d.ts +4 -1
  64. package/dist/types/common/dropdown/dropdown.types.d.ts +3 -2
  65. package/dist/types/common/infiniteBlock/useInfiniteScroll.d.ts +1 -1
  66. package/dist/types/common/listItem/index.d.ts +2 -0
  67. package/dist/types/common/listItem/listItem.d.ts +4 -0
  68. package/dist/types/common/listItem/listItem.types.d.ts +33 -0
  69. package/dist/types/common/menu/menu.types.d.ts +2 -2
  70. package/dist/types/common/menu/menuItem.d.ts +5 -2
  71. package/dist/types/common/themeProvider/theme.types.d.ts +4 -0
  72. package/dist/types/common/utils/navigation.d.ts +12 -0
  73. package/dist/types/index.d.ts +2 -0
  74. package/docs/ActionIcon.md +7 -3
  75. package/docs/AhroweProvider.md +2 -1
  76. package/docs/Breadcrumb.md +4 -24
  77. package/docs/Button.md +44 -9
  78. package/docs/CLAUDE.md +4 -0
  79. package/docs/ChatMessageList.md +5 -5
  80. package/docs/CommandPalette.md +1 -1
  81. package/docs/ConfigProvider.md +34 -1
  82. package/docs/Dropdown.md +17 -3
  83. package/docs/ListItem.md +97 -0
  84. package/docs/Menu.md +2 -2
  85. package/docs/ScrollbarProvider.md +1 -1
  86. package/package.json +1 -1
@@ -1,13 +1,17 @@
1
1
  import { IconContent } from '../types/icon.types.js';
2
2
  import { SlotClassNames, SlotStyles } from '../types/slots.types.js';
3
- import { ButtonProps } from '../button/button.types.js';
3
+ import { ButtonElementProps, ButtonLinkProps } from '../button/button.types.js';
4
4
  export type ActionIconSlots = 'root' | 'icon';
5
- /**
6
- * A Button with only an icon, sized by that icon, so every Button prop applies except the ones
7
- * that describe a label or a size.
8
- */
9
- export interface ActionIconProps extends Omit<ButtonProps, 'icon' | 'size' | 'children' | 'isLoading' | 'shortcut' | 'classNames' | 'styles'> {
5
+ type LabelAndSizeProps = 'icon' | 'size' | 'children' | 'isLoading' | 'shortcut' | 'classNames' | 'styles';
6
+ interface ActionIconOwnProps {
10
7
  icon: IconContent;
11
8
  classNames?: SlotClassNames<ActionIconSlots>;
12
9
  styles?: SlotStyles<ActionIconSlots>;
13
10
  }
11
+ /**
12
+ * A Button with only an icon, sized by that icon, so every Button prop applies except the ones
13
+ * that describe a label or a size. With `href` it is a link, as a Button is. Two branches rather
14
+ * than one `Omit<ButtonProps>`, which would merge them and lose `target` and the typed `onClick`.
15
+ */
16
+ export type ActionIconProps = (Omit<ButtonElementProps, LabelAndSizeProps> & ActionIconOwnProps) | (Omit<ButtonLinkProps, LabelAndSizeProps> & ActionIconOwnProps);
17
+ export {};
@@ -1,4 +1,4 @@
1
1
  import { ReactElement } from 'react';
2
2
  import { AhroweProviderProps } from './ahroweProvider.types.js';
3
- declare function AhroweProvider({ children, themes, currentThemeId, defaultProps, presentation, toast, scrollbars, }: AhroweProviderProps): ReactElement;
3
+ declare function AhroweProvider({ children, themes, currentThemeId, defaultProps, presentation, navigate, toast, scrollbars, }: AhroweProviderProps): ReactElement;
4
4
  export default AhroweProvider;
@@ -27,20 +27,14 @@ export declare enum ButtonType {
27
27
  Submit = "submit"
28
28
  }
29
29
  export type ButtonSlots = 'background' | 'content' | 'loadingContainer' | 'label' | 'shortcut';
30
- /**
31
- * `DataProps` supplies the `data-*` index signature, which `ButtonHTMLAttributes` doesn't cover.
32
- * `type`, `color` and `onClick` below deliberately narrow their inherited counterparts.
33
- */
34
- export interface ButtonProps extends DataProps, React.ButtonHTMLAttributes<HTMLButtonElement> {
30
+ interface ButtonOwnProps extends DataProps {
35
31
  className?: string;
36
32
  style?: CSSProperties;
37
- type?: ButtonType;
38
33
  /** Emphasis: `Filled`, `Light`, `Outlined` (default) or `Plain`. */
39
34
  styleType?: ButtonStyleType;
40
35
  /** A `ThemeColor` (default `Primary`) or any CSS colour, such as `'var(--brand)'`. */
41
36
  color?: ColorValue;
42
37
  size?: ButtonSize;
43
- onClick?: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
44
38
  children?: React.ReactNode;
45
39
  icon?: IconContent;
46
40
  /**
@@ -51,7 +45,30 @@ export interface ButtonProps extends DataProps, React.ButtonHTMLAttributes<HTMLB
51
45
  isLoading?: boolean;
52
46
  disabled?: boolean;
53
47
  title?: string;
54
- ref?: React.Ref<HTMLButtonElement>;
55
48
  classNames?: SlotClassNames<ButtonSlots>;
56
49
  styles?: SlotStyles<ButtonSlots>;
57
50
  }
51
+ /**
52
+ * A Button without `href`, a `<button>`. `DataProps` supplies the `data-*` index signature, which
53
+ * `ButtonHTMLAttributes` doesn't cover. `type` and `onClick` narrow their native counterparts.
54
+ */
55
+ export interface ButtonElementProps extends ButtonOwnProps, Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof ButtonOwnProps | 'type' | 'onClick'> {
56
+ href?: undefined;
57
+ type?: ButtonType;
58
+ onClick?: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void> | void;
59
+ ref?: React.Ref<HTMLButtonElement>;
60
+ }
61
+ /** A Button with `href`, an `<a>`: `target`, `rel` and `download` are the native ones. */
62
+ export interface ButtonLinkProps extends ButtonOwnProps, Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof ButtonOwnProps | 'type' | 'onClick' | 'href'> {
63
+ /**
64
+ * Renders the button as a link, so a long press, Ctrl/⌘-click and a middle click work as on any
65
+ * other: `tel:`, `mailto:`, a maps URL, a page of the app. A plain click on a page of the app goes
66
+ * through `ConfigProvider`'s `navigate` when it is set.
67
+ */
68
+ href: string;
69
+ type?: undefined;
70
+ onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => Promise<void> | void;
71
+ ref?: React.Ref<HTMLAnchorElement>;
72
+ }
73
+ export type ButtonProps = ButtonElementProps | ButtonLinkProps;
74
+ export {};
@@ -15,7 +15,7 @@ export interface ChatMessageListHandle {
15
15
  export interface ChatMessageListProps extends HtmlProps<HTMLDivElement> {
16
16
  /** `ChatMessage`s, and anything between them: a `Divider` with a date, a typing indicator. Oldest first. */
17
17
  children?: ReactNode;
18
- /** Loads older messages when the user scrolls to the top. Prepend them; the view stays where it was. Leave it out once there are no more. */
18
+ /** Loads older messages as the user nears the top. Prepend them; the view stays where it was. Leave it out once there are no more. */
19
19
  onLoadMore?: () => Promise<void>;
20
20
  ref?: Ref<ChatMessageListHandle>;
21
21
  /** Overrides the text ChatMessageList renders of its own. Merged per key. */
@@ -9,5 +9,6 @@ export declare const ConfigContext: React.Context<ComponentDefaults | null>;
9
9
  * consumer who missed one would get sheets in half their app.
10
10
  */
11
11
  export declare const PresentationContext: React.Context<Presentation | undefined>;
12
- declare function ConfigProvider({ children, defaultProps, presentation }: ConfigProviderProps): React.JSX.Element;
12
+ export declare const NavigateContext: React.Context<((path: string) => void) | undefined>;
13
+ declare function ConfigProvider({ children, defaultProps, presentation, navigate }: ConfigProviderProps): React.JSX.Element;
13
14
  export default ConfigProvider;
@@ -151,4 +151,12 @@ export interface ConfigProviderProps {
151
151
  * component-keyed entry in `defaultProps` overrides it, and an explicit prop overrides both.
152
152
  */
153
153
  presentation?: Presentation;
154
+ /**
155
+ * The app's router, for the links components render: Button and ListItem with an `href`, and
156
+ * Breadcrumb, Menu and CommandPalette entries. A plain click on a link within the app calls it
157
+ * with the path (`/clients/7?tab=projects`) instead of loading the page. Ctrl/⌘-click, a middle
158
+ * click, `target`, `download` and links to anywhere else stay with the browser. Unset, every
159
+ * link loads its page. A nested provider that does not set it keeps the outer one's.
160
+ */
161
+ navigate?: (path: string) => void;
154
162
  }
@@ -11,7 +11,10 @@ export interface CopyButtonLabels {
11
11
  /** Announced when the copy fails. Default: "Could not copy". */
12
12
  failed?: string;
13
13
  }
14
- export interface CopyButtonProps extends Omit<ActionIconProps, 'icon' | 'onCopy' | 'onError' | 'onClick' | 'ref' | 'classNames' | 'styles'> {
14
+ /** The `<button>` branch of ActionIcon: a copy button is never a link. */
15
+ export interface CopyButtonProps extends Omit<Extract<ActionIconProps, {
16
+ href?: undefined;
17
+ }>, 'href' | 'icon' | 'onCopy' | 'onError' | 'onClick' | 'ref' | 'classNames' | 'styles'> {
15
18
  /** Text written to the clipboard on click. */
16
19
  value: string;
17
20
  /** How long the copied state shows, in ms. Default: 2000. */
@@ -8,7 +8,7 @@ export interface DropdownItem {
8
8
  disabled?: boolean;
9
9
  }
10
10
  export type DropdownSlots = 'root' | 'label' | 'field' | 'control' | 'icon' | 'placeholder' | 'list' | 'option';
11
- export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'onMouseEnter' | 'onMouseLeave'> {
11
+ export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'onMouseEnter' | 'onMouseLeave' | 'onScroll'> {
12
12
  formValidator?: FormValidator | null;
13
13
  onMouseEnter?: ((event: React.MouseEvent) => void) | null;
14
14
  onMouseLeave?: ((event: React.MouseEvent) => void) | null;
@@ -27,7 +27,8 @@ export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'o
27
27
  isRow?: boolean;
28
28
  disabled?: boolean;
29
29
  readOnly?: boolean;
30
- onScroll?: () => Promise<void>;
30
+ /** Loads more items as the end of the open list comes near. Pass `undefined` once there are no more. */
31
+ onLoadMore?: () => Promise<void>;
31
32
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
32
33
  dropdownLabel?: string;
33
34
  isDropdownListWider?: boolean;
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- export declare function useInfiniteScroll(containerRef: React.RefObject<HTMLDivElement | null>, onScroll: () => Promise<void>, disabled: boolean): {
2
+ export declare function useInfiniteScroll(containerRef: React.RefObject<HTMLDivElement | null>, onLoadMore: (() => Promise<void>) | undefined): {
3
3
  isLoading: boolean;
4
4
  sentinelRef: React.RefObject<HTMLDivElement | null>;
5
5
  };
@@ -0,0 +1,2 @@
1
+ export { default } from './listItem.js';
2
+ export * from './listItem.types.js';
@@ -0,0 +1,4 @@
1
+ import { ReactElement } from 'react';
2
+ import { ListItemProps } from './listItem.types.js';
3
+ declare function ListItem({ icon, title, subtitle, endContent, href, target, rel, download, onClick, selected, chevron, className, style, classNames, styles: slotStyles, ...rest }: ListItemProps): ReactElement;
4
+ export default ListItem;
@@ -0,0 +1,33 @@
1
+ import { AnchorHTMLAttributes, CSSProperties, MouseEventHandler, ReactNode } from 'react';
2
+ import { IconContent } from '../types/icon.types.js';
3
+ import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types.js';
4
+ export type ListItemSlots = 'root' | 'icon' | 'labels' | 'trigger' | 'title' | 'subtitle' | 'endContent' | 'chevron';
5
+ export interface ListItemProps extends Omit<HtmlProps<HTMLDivElement>, 'title' | 'onClick'>, Pick<AnchorHTMLAttributes<HTMLAnchorElement>, 'target' | 'rel' | 'download'> {
6
+ /** Before the title: a FontAwesome icon or any element, such as an `Avatar`. */
7
+ icon?: IconContent;
8
+ /** The main line. Cut off with an ellipsis where it does not fit. */
9
+ title: ReactNode;
10
+ /** A smaller line under the title, cut off after two lines. */
11
+ subtitle?: ReactNode;
12
+ /**
13
+ * The trailing edge: a status `Chip`, a date, an amount, and controls of its own such as an
14
+ * edit button or a `Switch`, which keep working in a row that opens something.
15
+ */
16
+ endContent?: ReactNode;
17
+ /**
18
+ * Opens something from the whole row, as a link: Ctrl/⌘-click and a middle click open a new tab.
19
+ * A plain click on a page of the app goes through `ConfigProvider`'s `navigate` when it is set.
20
+ * `target`, `rel` and `download` apply to it.
21
+ */
22
+ href?: string;
23
+ /** Opens something from the whole row, as a button. With `href` as well, runs before the link is followed. */
24
+ onClick?: MouseEventHandler<HTMLElement>;
25
+ /** The row whose record is open: tinted, and the current page to a screen reader. */
26
+ selected?: boolean;
27
+ /** The chevron at the end. Shown for a row with an `href`, which takes the user somewhere. */
28
+ chevron?: boolean;
29
+ className?: string;
30
+ style?: CSSProperties;
31
+ classNames?: SlotClassNames<ListItemSlots>;
32
+ styles?: SlotStyles<ListItemSlots>;
33
+ }
@@ -26,8 +26,8 @@ export interface MenuAction {
26
26
  /** Renders in the destructive colour. For deleting, discarding, revoking. */
27
27
  danger?: boolean;
28
28
  /**
29
- * Followed after `onClick` and `onAction`. In `Menu` a real `<a>`, so Ctrl/⌘-click opens a new tab.
30
- * Loads the whole page: with a client-side router, navigate in `onClick` instead.
29
+ * Followed after `onClick` and `onAction`, through `ConfigProvider`'s `navigate` when it is set.
30
+ * In `Menu` a real `<a>`, so Ctrl/⌘-click opens a new tab.
31
31
  */
32
32
  href?: string;
33
33
  onClick?: () => void;
@@ -6,5 +6,8 @@ export declare function renderItemIcon(icon: MenuAction['icon'], reserveCell: bo
6
6
  className: string;
7
7
  style?: CSSProperties;
8
8
  }): ReactNode;
9
- /** Runs an action the way clicking it does: its own `onClick`, then `onAction`, then its `href`. */
10
- export declare function runMenuAction(action: MenuAction, onAction?: (id: string) => void): void;
9
+ /**
10
+ * Runs an action the way clicking it does: its own `onClick`, then `onAction`, then its `href`.
11
+ * `follow` is `useFollowHref()` where the caller can reach the router; `menuHotkeys` cannot.
12
+ */
13
+ export declare function runMenuAction(action: MenuAction, onAction?: (id: string) => void, follow?: (href: string) => void): void;
@@ -217,6 +217,10 @@ export interface ThemeVariables {
217
217
  '--multi-select-min-height'?: string;
218
218
  /** Maximum height of the open option panel. Falls back to 260px. */
219
219
  '--multi-select-panel-height'?: string;
220
+ /** Minimum height of a row. Falls back to 48px, the smallest comfortable touch target. */
221
+ '--list-item-min-height'?: string;
222
+ /** Background of a `selected` row. Falls back to a 12% tint of --primary-color. */
223
+ '--list-item-selected-background'?: string;
220
224
  /** Width a column collapses to once it has no cards (see `collapseEmptyColumns`). Falls back to 140px. */
221
225
  '--kanban-empty-column-width'?: string;
222
226
  '--option-picker-padding'?: string;
@@ -0,0 +1,12 @@
1
+ import { MouseEvent } from 'react';
2
+ type Navigate = (path: string) => void;
3
+ /**
4
+ * The click handler for an `<a>` a component renders: a plain click on a link within the app goes
5
+ * to `ConfigProvider`'s `navigate`, everything else to the browser. Call it after the consumer's own
6
+ * `onClick`, so a `preventDefault()` there still wins.
7
+ */
8
+ export declare function useLinkClick(): (event: MouseEvent<HTMLAnchorElement>) => void;
9
+ /** Follows a link from code, where no anchor was clicked: through the router where it can. */
10
+ export declare function followHref(href: string, navigate?: Navigate): void;
11
+ export declare function useFollowHref(): (href: string) => void;
12
+ export {};
@@ -82,6 +82,8 @@ export { default as KanbanBoard } from './common/kanbanBoard/index.js';
82
82
  export * from './common/kanbanBoard/index.js';
83
83
  export { default as KlipyPicker } from './common/klipyPicker/index.js';
84
84
  export * from './common/klipyPicker/index.js';
85
+ export { default as ListItem } from './common/listItem/index.js';
86
+ export * from './common/listItem/index.js';
85
87
  export { default as Loader } from './common/loader/index.js';
86
88
  export * from './common/loader/index.js';
87
89
  export { default as LoadingOverlay } from './common/loadingOverlay/index.js';
@@ -9,18 +9,21 @@
9
9
  **Requires:** `@fortawesome/free-solid-svg-icons` peer dependency.
10
10
 
11
11
  An ActionIcon is a [Button](Button.md) holding only an icon and sized by it: a real `<button>`
12
- with Button's hover, focus ring and ripple. It takes Button's `styleType` and `color`, plain and
12
+ with Button's hover, focus ring and ripple, or with an `href` a link, as a Button is. It takes Button's `styleType` and `color`, plain and
13
13
  in the surrounding text colour by default, so it fits whatever it sits on.
14
14
 
15
15
  ```tsx
16
16
  import { ActionIcon, ButtonStyleType, ThemeColor } from '@ahrowe/ui';
17
- import { faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
17
+ import { faPhone, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
18
18
 
19
19
  <ActionIcon icon={faXmark} aria-label="Close" onClick={onClose} />
20
20
 
21
21
  // A destructive row action
22
22
  <ActionIcon icon={faTrash} color={ThemeColor.Error} title="Delete item" onClick={handleDelete} />
23
23
 
24
+ // A link: a long press copies the number
25
+ <ActionIcon icon={faPhone} href="tel:+431234567" aria-label="Call" />
26
+
24
27
  // Filled, for an action that should stand out
25
28
  <ActionIcon icon={faTrash} styleType={ButtonStyleType.Filled} color={ThemeColor.Error} aria-label="Delete" />
26
29
  ```
@@ -35,7 +38,8 @@ import { faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
35
38
  | `title` | `string` | Tooltip. Give an icon-only button an `aria-label` as well |
36
39
  | `onClick` | `(event: MouseEvent<HTMLButtonElement>) => void \| Promise<void>` | Click handler |
37
40
  | `disabled` | `boolean` | Fades it and blocks clicks |
38
- | `ref` | `Ref<HTMLButtonElement>` | Ref to the button |
41
+ | `href` | `string` | Renders it as an `<a>`, with `target`, `rel` and `download`. See [Button.md](Button.md) |
42
+ | `ref` | `Ref<HTMLButtonElement>` | Ref to the button, or `Ref<HTMLAnchorElement>` with an `href` |
39
43
 
40
44
  Every other Button prop and native `<button>` attribute passes through, except `size`,
41
45
  `children`, `isLoading` and `shortcut`. The box is the icon plus `4px` on each side; set `width`
@@ -40,6 +40,7 @@ Every prop is optional. `<AhroweProvider><App /></AhroweProvider>` applies `defa
40
40
  | `currentThemeId` | `string` | the first theme without a `baseThemeId` | The `id` of the theme to apply |
41
41
  | `defaultProps` | `ComponentDefaults` | | Default props per component. See [ConfigProvider.md](ConfigProvider.md) |
42
42
  | `presentation` | `Presentation` | | How every sheet-capable component presents. See [ConfigProvider.md](ConfigProvider.md) |
43
+ | `navigate` | `(path: string) => void` | | The app's router, for the links components render. Usually set on a `ConfigProvider` inside the router instead, see [ConfigProvider.md](ConfigProvider.md) |
43
44
  | `toast` | `ToastContextConfig` | | Settings for every toast `showToast` shows. See [Toast.md](Toast.md) |
44
45
  | `scrollbars` | `boolean` | `true` | Styles every scrollbar inside the app in the theme's colours. See **Scrollbars** |
45
46
 
@@ -51,7 +52,7 @@ On the server, and while hydrating, nothing is portalled: the server has nowhere
51
52
 
52
53
  ## Nesting
53
54
 
54
- A nested `AhroweProvider` applies its `themes`, `currentThemeId`, `defaultProps` and `presentation` to its section. Its `defaultProps` merge with the outer ones per component, the same as nested `ConfigProvider`s. Toasts, scrollbars and the portal element stay with the outermost provider, so a nested `toast` or `scrollbars` is ignored, with a warning.
55
+ A nested `AhroweProvider` applies its `themes`, `currentThemeId`, `defaultProps`, `presentation` and `navigate` to its section. Its `defaultProps` merge with the outer ones per component, the same as nested `ConfigProvider`s. Toasts, scrollbars and the portal element stay with the outermost provider, so a nested `toast` or `scrollbars` is ignored, with a warning.
55
56
 
56
57
  Only a provider inside another `AhroweProvider` counts as nested. Put the outer one at the root first: one placed around a section of an app that has none renders the portal element inside that section, where an `overflow: hidden` or a `transform` around it clips every panel.
57
58
 
@@ -30,28 +30,8 @@ import { faHouse, faFolder } from '@fortawesome/free-solid-svg-icons';
30
30
  ]}
31
31
  />
32
32
 
33
- // Client-side routing (e.g. react-router): keep href for a real, right-clickable link,
34
- // and use onClick to intercept the native navigation with the router's own navigate()
35
- import { useNavigate } from 'react-router-dom';
36
-
37
- function Example() {
38
- const navigate = useNavigate();
39
- return (
40
- <Breadcrumb
41
- items={[
42
- {
43
- label: 'Home',
44
- href: '/',
45
- onClick: (event) => {
46
- event.preventDefault();
47
- navigate('/');
48
- },
49
- },
50
- { label: 'Current page' },
51
- ]}
52
- />
53
- );
54
- }
33
+ // With a client-side router, set `navigate` once on ConfigProvider (ConfigProvider.md):
34
+ // a plain click on a crumb then goes through the router, a middle click still opens a new tab.
55
35
 
56
36
  // A disabled crumb: dimmed, not clickable, regardless of href/onClick
57
37
  <Breadcrumb
@@ -86,9 +66,9 @@ function Example() {
86
66
  |-------|------|-------------|
87
67
  | `id` | `string` | Optional stable key; falls back to the item's index |
88
68
  | `label` | `ReactNode` | The crumb's text |
89
- | `href` | `string` | Renders the crumb as a real `<a>`. Omit to render a plain clickable element (with `onClick`) or static text |
69
+ | `href` | `string` | Renders the crumb as a real `<a>`, followed through `ConfigProvider`'s `navigate` when it is set. Omit to render a plain clickable element (with `onClick`) or static text |
90
70
  | `icon` | `IconDefinition \| ReactElement` | Optional leading icon — FontAwesome icon or any React element |
91
- | `onClick` | `(event: MouseEvent) => void` | Click handler. Works alongside `href`: call `event.preventDefault()` to stop the native navigation and hand off to a client-side router's own navigate function (see the react-router example above). Also works on its own, with no `href` |
71
+ | `onClick` | `(event: MouseEvent) => void` | Click handler. Alongside `href` it runs first, and `event.preventDefault()` stops the link from being followed. Also works on its own, with no `href` |
92
72
  | `disabled` | `boolean` | Dims the crumb and makes it non-interactive, regardless of `href`/`onClick` |
93
73
 
94
74
  **Key props:**
package/docs/Button.md CHANGED
@@ -12,7 +12,7 @@ works with any colour. The colours are the library-wide `ThemeColor`s or any CSS
12
12
 
13
13
  ```tsx
14
14
  import { Button, ButtonStyleType, ButtonSize, ButtonType, ThemeColor, useHotkeys } from '@ahrowe/ui';
15
- import { faPlus, faSave, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
15
+ import { faLocationArrow, faPhone, faPlus, faSave, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
16
16
 
17
17
  // The main action
18
18
  <Button styleType={ButtonStyleType.Filled} onClick={handleSave}>Save</Button>
@@ -67,6 +67,10 @@ useHotkeys({ 'mod+s': handleSave });
67
67
 
68
68
  // Submit button in a form
69
69
  <Button type={ButtonType.Submit} styleType={ButtonStyleType.Filled}>Submit</Button>
70
+
71
+ // A link: a long press copies the number, a middle click opens a new tab
72
+ <Button href="tel:+431234567" icon={faPhone}>Call</Button>
73
+ <Button href={mapsUrl} target="_blank" icon={faLocationArrow}>Directions</Button>
70
74
  ```
71
75
 
72
76
  **Enums:**
@@ -83,19 +87,37 @@ useHotkeys({ 'mod+s': handleSave });
83
87
  | `styleType` | `ButtonStyleType` | `Filled`: solid fill, for the one main action. `Light`: a tint of the colour behind text in the colour. `Outlined` (default): border and text in the colour. `Plain`: text in the colour, no fill or border, for toolbar, icon and close buttons |
84
88
  | `color` | `ColorValue` | A `ThemeColor` (default `Primary`) or any CSS colour. `Neutral` takes the surrounding text colour, with a `--border-color` outline; `Error` is for destructive actions. See [ThemeColor.md](ThemeColor.md) |
85
89
  | `size` | `ButtonSize` | Size variant. `Icon` strips all padding, inherits the surrounding text size, and centres a lone icon — set the width/height yourself |
86
- | `type` | `ButtonType` | HTML button type |
90
+ | `type` | `ButtonType` | HTML button type. Not on a link |
91
+ | `href` | `string` | Renders an `<a>` instead of a `<button>`, see **As a link** |
87
92
  | `icon` | `IconDefinition \| ReactElement` | FontAwesome icon or any React element, shown before label |
88
93
  | `shortcut` | `string` | A combo in [useHotkeys](Hooks.md) spelling, e.g. `'mod+s'`. Shown after the label, written for the platform. Binding it is yours, see below |
89
- | `isLoading` | `boolean` | Replaces content with a [Loader](Loader.md) in the button's text colour. Blocks clicks like `disabled` but keeps full colour |
94
+ | `isLoading` | `boolean` | Replaces content with a [Loader](Loader.md) in the button's text colour. Blocks clicks and a form submit like `disabled`, but keeps full colour and the keyboard focus. |
90
95
  | `disabled` | `boolean` | Fades the button to half opacity |
91
96
  | `onClick` | `(e: MouseEvent) => void \| Promise<void>` | |
92
97
 
93
- `ButtonProps` extends `React.ButtonHTMLAttributes<HTMLButtonElement>`, so every native `<button>`
94
- attribute is accepted and forwarded — pointer handlers (`onPointerDown` and friends, for
95
- press-and-hold controls that act on press rather than release), `onContextMenu`, `tabIndex`,
96
- `name`, `value`, `form`, `autoFocus`, and the rest. `type` and `onClick` are narrowed: `type`
97
- takes the `ButtonType` enum, and `onClick` may return a promise. `color` is Button's own prop, not
98
- the obsolete HTML attribute.
98
+ Without an `href`, every native `<button>` attribute is accepted and forwarded — pointer handlers
99
+ (`onPointerDown` and friends, for press-and-hold controls that act on press rather than release),
100
+ `onContextMenu`, `tabIndex`, `name`, `value`, `form`, `autoFocus`, and the rest. `type` and
101
+ `onClick` are narrowed: `type` takes the `ButtonType` enum, and `onClick` may return a promise.
102
+ `color` is Button's own prop, not the obsolete HTML attribute. With an `href`, the native `<a>`
103
+ attributes take their place, see below.
104
+
105
+ ## As a link
106
+
107
+ With an `href` a Button is an `<a>` that looks like a button, so it behaves like a link: a long
108
+ press offers to copy the address, and Ctrl/⌘-click and a middle click open a new tab. Use it for
109
+ anything that goes somewhere: `tel:`, `mailto:`, a maps URL, a page of your app. A button whose
110
+ `onClick` sets `window.location` gets none of that.
111
+
112
+ - **Types.** `ButtonProps` is two shapes, told apart by `href`. The link takes the native `<a>`
113
+ attributes, `target`, `rel` and `download` among them, types `onClick` and `ref` for the `<a>`,
114
+ and has no `type`. An `href` that may be `undefined` renders a `<button>` while it is, and an
115
+ `onClick` beside it needs its event typed as `MouseEvent<HTMLElement>`.
116
+ - **Pages of your app** load the whole page unless `ConfigProvider` has a `navigate`, which then
117
+ takes a plain click, see [ConfigProvider.md](ConfigProvider.md).
118
+ - **`disabled`** drops the `href`: an `<a>` cannot be disabled, so it leaves the tab order and is
119
+ announced as a dimmed link. **`isLoading`** keeps it, and with it the focus, but is not followed.
120
+ - **`target="_blank"`** needs no `rel="noopener"`: every browser the library supports implies it.
99
121
 
100
122
  **Shortcuts are drawn, not bound.** `shortcut` writes the combo in a pill after the label,
101
123
  `⌘S` on an Apple platform and `Ctrl+S` elsewhere. It also sets `aria-keyshortcuts`, so a screen
@@ -139,6 +161,19 @@ A Button is sized by its font. `--default-font-size` is the base, `Medium` is th
139
161
 
140
162
  `ButtonSize.Icon` is the exception: it inherits the surrounding text size and has no padding at all, because the caller sets the box.
141
163
 
164
+ ## Migration from the `ButtonProps` interface
165
+
166
+ Before 0.51, `ButtonProps` and `ActionIconProps` were interfaces for a `<button>`. Each is now a
167
+ union of a button and a link shape, told apart by `href`. Code that renders a Button compiles
168
+ unchanged; a type built on one may not, because an interface cannot extend a union and `Omit` on
169
+ one keeps only what both shapes share:
170
+
171
+ | Before | After |
172
+ |--------|-------|
173
+ | `interface SaveButtonProps extends ButtonProps { … }` | `type SaveButtonProps = ButtonProps & { … }`, which keeps both shapes, so the wrapper takes an `href` too |
174
+ | `interface MyIconProps extends ActionIconProps { … }` | `type MyIconProps = ActionIconProps & { … }` |
175
+ | `Omit<ButtonProps, 'styleType'>` | `Omit<ButtonElementProps, 'styleType'>` for a wrapper that is only ever a `<button>` |
176
+
142
177
  ## Migration from `ButtonSize.Default`
143
178
 
144
179
  Before 0.45, `ButtonSize.Default` was a second name for `Medium`, which is now the default. Replace
package/docs/CLAUDE.md CHANGED
@@ -40,6 +40,9 @@ enums and slot names. Do not infer props from a component's name.
40
40
  emphasis, the same way everywhere: `Filled` for the one main action of a region (at most one),
41
41
  `Light` for status and selection, `Outlined` for secondary actions, `Plain` for tertiary ones.
42
42
  - **`icon` props** take a FontAwesome `IconDefinition` or any React element.
43
+ - **Links.** An `href` (Button, ActionIcon, ListItem, Breadcrumb, Menu, CommandPalette) renders a
44
+ real `<a>`. With a client-side router, set `navigate` on a `ConfigProvider` inside the router
45
+ once, or every plain click loads the page (ConfigProvider.md).
43
46
  - **`ref`** is a plain prop, there is no `forwardRef`. It lands on the element you would reach for:
44
47
  Input, Textarea, NumberInput and SearchInput hand out their inner `<input>`, ChatComposer its
45
48
  `<textarea>`. Timer, Flip, Tilt, Ripple, VirtualList, ChatMessageList, RoomDrawer and RoomViewer
@@ -106,6 +109,7 @@ enums and slot names. Do not infer props from a component's name.
106
109
  | InteractableDiv.md | InteractableDiv | Accessible `<div>` that behaves like a button — click and keyboard (Enter/Space) events, with proper ARIA semantics |
107
110
  | KanbanBoard.md | KanbanBoard, KanbanColumn | Drag-and-drop board with columns — task management, workflow stages, status boards (e.g. Todo / In Progress / Done) |
108
111
  | KlipyPicker.md | KlipyPicker | GIF picker backed by the Klipy API — chat reactions, message composers, content authoring tools |
112
+ | ListItem.md | ListItem | One row of a list that stands for a record: a customer, an invoice, a delivery note |
109
113
  | Loader.md | Loader, LoaderType, ThemeColor | A loading indicator for something in progress with no known end: a fetch, a save, a connection |
110
114
  | LoadingOverlay.md | LoadingOverlay, Loader, LoaderType | Covering content while it reloads: a table refreshing, a form saving, a panel fetching its next page |
111
115
  | Menu.md | Menu, menuHotkeys | A list of actions hanging off a trigger — a row's "more" button, a toolbar overflow, a context menu on a card |
@@ -83,12 +83,12 @@ button while it does:
83
83
  bottom by hand does the same.
84
84
  - **After sending**, call `scrollToBottom()` on the handle, or a user who sent from further up
85
85
  never sees their own message arrive.
86
- - **Older messages.** With `onLoadMore`, reaching the top calls it and shows a loader there.
86
+ - **Older messages.** With `onLoadMore`, nearing the top calls it and shows a loader there.
87
87
  Prepend what it loads; the message that was at the top stays exactly where it was, at the very
88
88
  top too, where the browser's own scroll anchoring does nothing and Safari has none at all. It is
89
- called again while the loaded page still leaves the top in view. If it rejects, it is called
90
- again after three seconds if the top is still in view, or whenever the user next scrolls there.
91
- Pass `undefined` once there is nothing older.
89
+ called again while the loaded page still leaves the top near, but not after one that added
90
+ nothing. If it rejects, it is called again after three seconds if the top is still near, or
91
+ whenever the user next scrolls there. Pass `undefined` once there is nothing older.
92
92
 
93
93
  **Sizing.** The list scrolls itself and fills the height it is given: `flex: 1` in a column, as
94
94
  above, or a `height`. A conversation shorter than the list sits at the bottom, by the composer. To
@@ -99,7 +99,7 @@ start at the top instead, as an AI chat often does: `styles={{ content: { margin
99
99
  | Prop | Type | Description |
100
100
  |------|------|-------------|
101
101
  | `children` | `ReactNode` | Messages, oldest first, and anything between them: a `Divider` with a date, a typing indicator |
102
- | `onLoadMore` | `() => Promise<void>` | Loads older messages when the top comes into view, see above |
102
+ | `onLoadMore` | `() => Promise<void>` | Loads older messages as the top comes near, see above |
103
103
  | `ref` | `Ref<ChatMessageListHandle>` | `scrollToBottom()`: back to the newest message, smoothly unless the user prefers reduced motion, and following again |
104
104
 
105
105
  **Accessibility:** the scrolling element is a `role="log"`, which screen readers announce
@@ -73,7 +73,7 @@ On a small touch screen it can present as a bottom sheet like every `Modal`: pas
73
73
  | `isLoading` | `boolean` | Shows the `loading` label instead of `empty` while nothing matches |
74
74
  | `presentation` | `Presentation` | Centred dialog (default), bottom sheet, or a sheet only on a small touch screen |
75
75
 
76
- A command is a `MenuAction`: `id`, `label`, `icon`, `shortcut`, `keywords`, `disabled`, `danger`, `href` and `onClick`. A command with an `href` goes there after running. See [Menu.md](Menu.md).
76
+ A command is a `MenuAction`: `id`, `label`, `icon`, `shortcut`, `keywords`, `disabled`, `danger`, `href` and `onClick`. A command with an `href` goes there after running, through `ConfigProvider`'s `navigate` when it is set. See [Menu.md](Menu.md).
77
77
 
78
78
  **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ CommandPalette: { presentation: Presentation.Auto } }}`. See [ConfigProvider.md](ConfigProvider.md).
79
79
 
@@ -7,7 +7,7 @@
7
7
  **Import:** `import { ConfigProvider } from '@ahrowe/ui'`
8
8
  **Types:** `import type { ComponentDefaults, ConfigProviderProps } from '@ahrowe/ui'`
9
9
 
10
- At the app root, pass `defaultProps` and `presentation` to [AhroweProvider](AhroweProvider.md), which includes a ConfigProvider. Use ConfigProvider on its own for a section with defaults of its own:
10
+ At the app root, pass `defaultProps`, `presentation` and `navigate` to [AhroweProvider](AhroweProvider.md), which includes a ConfigProvider. Use ConfigProvider on its own for a section with defaults of its own:
11
11
 
12
12
  ```tsx
13
13
  import { AhroweProvider, ConfigProvider } from '@ahrowe/ui';
@@ -58,6 +58,7 @@ So a global default only fills in props a given instance left out — any instan
58
58
  |------|------|-------------|
59
59
  | `defaultProps` | `ComponentDefaults` | Default props keyed by component name, e.g. `{ Input: { alwaysFloatLabel: true } }` |
60
60
  | `presentation` | `Presentation` | How every sheet-capable component presents by default — one switch for all of them (see below) |
61
+ | `navigate` | `(path: string) => void` | The app's router, for every link a component renders (see below) |
61
62
  | `children` | `ReactNode` | Your app content |
62
63
 
63
64
  ## App-wide `presentation`
@@ -90,6 +91,38 @@ It resolves *under* the component-keyed defaults, so the full order for a `prese
90
91
 
91
92
  A nested provider inherits it unless it sets its own, the same way `defaultProps` compose. Everything built on those four components follows along: `SplitButton`, `ColorPicker`, `Breadcrumb`, `VirtualList`, and `DatePicker`/`TimeInput`/`KlipyPicker` in either their menu or their `isModal` mode. `InputDropdown` and `Tooltip` are deliberately never sheets and ignore it.
92
93
 
94
+ ## Client-side routing (`navigate`)
95
+
96
+ Button, ActionIcon and ListItem with an `href`, and Breadcrumb, Menu and CommandPalette entries with one, are real links: a long press, Ctrl/⌘-click and a middle click work on them as on any `<a>`. Without `navigate` a plain click loads the page. With it, a plain click on a page of the app calls `navigate` with the path instead:
97
+
98
+ ```tsx
99
+ import { Outlet, useNavigate } from 'react-router-dom';
100
+ import { ConfigProvider } from '@ahrowe/ui';
101
+
102
+ // The root route's element. useNavigate needs the router, and AhroweProvider is usually outside it.
103
+ function Root() {
104
+ const navigate = useNavigate();
105
+ return (
106
+ <ConfigProvider navigate={navigate}>
107
+ <Outlet />
108
+ </ConfigProvider>
109
+ );
110
+ }
111
+ ```
112
+
113
+ It is called with the path, search and hash (`/clients/7?tab=projects`) for a plain left click on an `http:` or `https:` link to the page's own origin. The browser keeps everything else: a click with Ctrl, ⌘, Shift or Alt, a middle click, a link with a `target` or `download`, another site, `mailto:`, `tel:`, a `blob:` URL, and a jump to an anchor on the current page. An `onClick` of your own runs first, and a `preventDefault()` in it keeps `navigate` out.
114
+
115
+ - **Write absolute paths.** An `href` is resolved against the page's URL, like any link, because that is what a middle click does; `navigate` gets the resolved path. A router resolves a relative path against the route instead, so `href="projects/1"` on `/clients/7` is `/clients/projects/1`, not where `navigate('projects/1')` would go.
116
+ - **`navigate` decides.** It gets every path of the page's own origin, also one the router does not own: a server route such as `/logout`, a file under `/api`, a page outside a `basename`. Hand those back to the browser with `window.location.assign(path)`. A `basename` is part of the `href` but not of the path the router's `navigate` takes, so strip it:
117
+
118
+ ```tsx
119
+ navigate={(path) => {
120
+ if (path === BASENAME || path.startsWith(`${BASENAME}/`)) navigate(path.slice(BASENAME.length) || '/');
121
+ else window.location.assign(path);
122
+ }}
123
+ ```
124
+ - **Two places it cannot reach.** Toasts render in `AhroweProvider`'s own host, outside a `ConfigProvider` placed inside the router. `menuHotkeys` runs outside React, so an entry with an `href` run from its shortcut loads the page.
125
+
93
126
  ## Supported components
94
127
 
95
128
  Each entry is a `Partial<...Props>`, so any of that component's props can be defaulted:
package/docs/Dropdown.md CHANGED
@@ -36,13 +36,16 @@ const statusValidator = new FormValidator('', [Validators.required()]);
36
36
  // Bottom sheet on small touch screens, anchored to the input everywhere else
37
37
  <Dropdown label="Status" items={items} value={status} onChange={onChange} presentation={Presentation.Auto} />
38
38
 
39
- // Infinite scroll support
39
+ // Infinite scroll: more items as the end of the open list comes near
40
40
  <Dropdown
41
41
  items={items}
42
- onScroll={async () => { await loadMoreItems(); }}
42
+ onLoadMore={hasMore ? loadMoreItems : undefined}
43
43
  />
44
44
  ```
45
45
 
46
+ Loading on scroll suits a list people browse. To find one entry among hundreds, typing beats
47
+ scrolling: InputDropdown filters as the user types.
48
+
46
49
  **Key props:**
47
50
 
48
51
  | Prop | Type | Description |
@@ -57,7 +60,7 @@ const statusValidator = new FormValidator('', [Validators.required()]);
57
60
  | `disabled` | `boolean` | Greys the field out and prevents interaction |
58
61
  | `readOnly` | `boolean` | Locks the selection without the disabled styling — the field keeps its normal appearance and stays focusable, but the menu won't open and the value can't change (the chevron is hidden). Default `false` |
59
62
  | `placeholder` | `string` | Placeholder when no selection |
60
- | `onScroll` | `() => Promise<void>` | Load more on list scroll |
63
+ | `onLoadMore` | `() => Promise<void>` | Loads more items as the end of the open list comes near, with a spinner meanwhile. Append what it loads. Called again while the end stays near, until a load appends nothing; pass `undefined` once there are no more to save that last call. A rejected load is tried again after three seconds |
61
64
  | `useMatLabelStyle` | `boolean` | Material-style floating label |
62
65
  | `presentation` | `Presentation` | Where the list opens: anchored to the input (`Default`), always a bottom sheet, or a sheet only on a small touch screen. Imported from the same package: `import { Presentation } from '@ahrowe/ui'` |
63
66
  | `alwaysFloatLabel` | `boolean` | Keep the material label floated in the border notch even when nothing is selected, so an empty dropdown reads as blank instead of showing the label as if it were the selected value. Only applies with `useMatLabelStyle` (default) |
@@ -87,3 +90,14 @@ outside (a wrapper, a grid column, `style={{ maxWidth: 240 }}`) rather than expe
87
90
  to the label: everything inside is absolutely positioned, so it has no width of its own to fall back on.
88
91
 
89
92
  **Slots:** `root` `label` `field` `control` `icon` `placeholder` `list` `option`
93
+
94
+ ## Migration from `onScroll`
95
+
96
+ Before 0.51 the load-more callback was `onScroll`, and it was never called. TypeScript points at
97
+ every use:
98
+
99
+ | Before | After |
100
+ |--------|-------|
101
+ | `onScroll={…}` | `onLoadMore={…}` |
102
+
103
+ Loading now actually happens, so pass `undefined` once there is nothing more to load.