@ahrowe/ui 0.27.0 → 0.28.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 (72) hide show
  1. package/dist/esm/common/accordion/accordion.mjs +1 -1
  2. package/dist/esm/common/accordion/accordion.mjs.map +1 -1
  3. package/dist/esm/common/colorPicker/colorPicker.mjs +1 -1
  4. package/dist/esm/common/colorPicker/colorPicker.mjs.map +1 -1
  5. package/dist/esm/common/confirmModal/confirmModal.mjs +1 -1
  6. package/dist/esm/common/confirmModal/confirmModal.mjs.map +1 -1
  7. package/dist/esm/common/dropZone/dropZone.mjs +1 -1
  8. package/dist/esm/common/dropZone/dropZone.mjs.map +1 -1
  9. package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +1 -1
  10. package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +1 -1
  11. package/dist/esm/common/hooks/useAnchorTracking.mjs +2 -0
  12. package/dist/esm/common/hooks/useAnchorTracking.mjs.map +1 -0
  13. package/dist/esm/common/idleManager/idleManager.mjs +1 -1
  14. package/dist/esm/common/idleManager/idleManager.mjs.map +1 -1
  15. package/dist/esm/common/input/input.mjs +1 -1
  16. package/dist/esm/common/input/input.mjs.map +1 -1
  17. package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
  18. package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
  19. package/dist/esm/common/klipyPicker/components/gifView/gifView.mjs +1 -1
  20. package/dist/esm/common/klipyPicker/components/gifView/gifView.mjs.map +1 -1
  21. package/dist/esm/common/multiSelect/multiSelect.mjs +2 -0
  22. package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -0
  23. package/dist/esm/common/multiSelect/multiSelect.module.mjs +2 -0
  24. package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -0
  25. package/dist/esm/common/overscroll/overscroll.mjs +1 -1
  26. package/dist/esm/common/overscroll/overscroll.mjs.map +1 -1
  27. package/dist/esm/common/pagination/pagination.mjs +2 -0
  28. package/dist/esm/common/pagination/pagination.mjs.map +1 -0
  29. package/dist/esm/common/pagination/pagination.module.mjs +2 -0
  30. package/dist/esm/common/pagination/pagination.module.mjs.map +1 -0
  31. package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
  32. package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
  33. package/dist/esm/common/timer/timer.mjs.map +1 -1
  34. package/dist/esm/common/tree/flattenTree.mjs +2 -0
  35. package/dist/esm/common/tree/flattenTree.mjs.map +1 -0
  36. package/dist/esm/common/tree/tree.mjs +2 -0
  37. package/dist/esm/common/tree/tree.mjs.map +1 -0
  38. package/dist/esm/common/tree/tree.module.mjs +2 -0
  39. package/dist/esm/common/tree/tree.module.mjs.map +1 -0
  40. package/dist/esm/common/virtualList/virtualList.mjs +1 -1
  41. package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
  42. package/dist/esm/common/virtualList/virtualRow.mjs.map +1 -1
  43. package/dist/esm/index.mjs +1 -1
  44. package/dist/esm/services/formValidation/validatableComponent.mjs.map +1 -1
  45. package/dist/index.cjs +3 -3
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/style.css +1 -1
  48. package/dist/types/package/common/configProvider/configProvider.types.d.ts +6 -0
  49. package/dist/types/package/common/floatingMenu/useFloatingPosition.d.ts +4 -6
  50. package/dist/types/package/common/hooks/useAnchorTracking.d.ts +40 -0
  51. package/dist/types/package/common/inputDropdown/inputDropdown.types.d.ts +6 -0
  52. package/dist/types/package/common/multiSelect/index.d.ts +2 -0
  53. package/dist/types/package/common/multiSelect/multiSelect.d.ts +4 -0
  54. package/dist/types/package/common/multiSelect/multiSelect.types.d.ts +62 -0
  55. package/dist/types/package/common/pagination/index.d.ts +2 -0
  56. package/dist/types/package/common/pagination/pagination.d.ts +4 -0
  57. package/dist/types/package/common/pagination/pagination.types.d.ts +30 -0
  58. package/dist/types/package/common/popover/usePopoverPosition.d.ts +4 -2
  59. package/dist/types/package/common/tree/flattenTree.d.ts +3 -0
  60. package/dist/types/package/common/tree/index.d.ts +3 -0
  61. package/dist/types/package/common/tree/tree.d.ts +4 -0
  62. package/dist/types/package/common/tree/tree.types.d.ts +62 -0
  63. package/dist/types/package/common/virtualList/virtualList.types.d.ts +8 -0
  64. package/dist/types/package/common/virtualList/virtualRow.d.ts +1 -1
  65. package/dist/types/package/index.d.ts +6 -0
  66. package/docs/CLAUDE.md +3 -0
  67. package/docs/InputDropdown.md +3 -0
  68. package/docs/MultiSelect.md +100 -0
  69. package/docs/Pagination.md +72 -0
  70. package/docs/Tree.md +95 -0
  71. package/docs/VirtualList.md +1 -0
  72. package/package.json +2 -2
@@ -29,6 +29,9 @@ import { FlipProps } from '../flip/flip.types';
29
29
  import { OptionPickerProps } from '../optionPicker/optionPicker.types';
30
30
  import { TimerProps } from '../timer/timer.types';
31
31
  import { OtpInputProps } from '../otpInput/otpInput.types';
32
+ import { MultiSelectProps } from '../multiSelect/multiSelect.types';
33
+ import { PaginationProps } from '../pagination/pagination.types';
34
+ import { TreeProps } from '../tree/tree.types';
32
35
  import { RoomDrawerProps } from '../roomDrawer/roomDrawer.types';
33
36
  import { RoomViewerProps } from '../roomViewer/roomViewer.types';
34
37
  import { FloatingMenuProps, Presentation } from '../floatingMenu/floatingMenu.types';
@@ -79,6 +82,9 @@ export interface ComponentDefaults {
79
82
  OptionPicker?: Partial<OptionPickerProps>;
80
83
  Timer?: Partial<TimerProps>;
81
84
  OtpInput?: Partial<OtpInputProps>;
85
+ MultiSelect?: Partial<MultiSelectProps>;
86
+ Pagination?: Partial<PaginationProps>;
87
+ Tree?: Partial<TreeProps>;
82
88
  RoomDrawer?: Partial<RoomDrawerProps>;
83
89
  RoomViewer?: Partial<RoomViewerProps>;
84
90
  FloatingMenu?: Partial<FloatingMenuProps>;
@@ -13,17 +13,15 @@ export interface UseFloatingPositionArgs {
13
13
  placement?: 'top' | 'bottom';
14
14
  }
15
15
  /**
16
- * Keeps a portaled, `position: fixed` popup anchored to a trigger element: tracks the
17
- * trigger across scroll/resize on every real scrolling ancestor, flips above the trigger
18
- * when there's no room below, and hides (not closes) the popup when the trigger itself
16
+ * Keeps a portaled, `position: fixed` popup anchored to a trigger element: flips above the
17
+ * trigger when there's no room below, and hides (not closes) the popup when the trigger itself
19
18
  * scrolls behind a clipping ancestor — matching Floating UI's autoUpdate + hide middleware.
20
19
  * It also shifts the popup horizontally to stay within the viewport: `align`/RTL-style CSS
21
20
  * only positions the panel relative to the anchor's own box and has no awareness of where
22
21
  * that box actually sits on screen, so a trigger near a screen edge would otherwise let the
23
22
  * panel overflow it with no correction.
24
23
  *
25
- * Position/visibility are written straight to the DOM via refs, not through React state:
26
- * going through setState -> re-render -> commit adds a round-trip that lags behind the
27
- * browser's own scroll painting by at least a frame.
24
+ * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and
25
+ * re-running on scroll, viewport resize and panel size changes.
28
26
  */
29
27
  export declare function useFloatingPosition({ isOpen, triggerRef, anchorRef, panelRef, flipClassName, placement }: UseFloatingPositionArgs): void;
@@ -0,0 +1,40 @@
1
+ import { RefObject } from 'react';
2
+ export interface UseAnchorTrackingArgs {
3
+ isOpen: boolean;
4
+ /** The element the popup is anchored to. Its scrollable ancestors are what get tracked. */
5
+ triggerRef: RefObject<HTMLElement | null>;
6
+ /**
7
+ * Every element the positioning maths needs. Tracking starts only once all of them, plus the
8
+ * trigger, are mounted. Read fresh on each setup, so an inline array literal is fine.
9
+ */
10
+ elementRefs: RefObject<HTMLElement | null>[];
11
+ /**
12
+ * Positions the popup. Called once on setup and then on every scroll, resize and tracked size
13
+ * change. Reads the caller's own refs for everything beyond the two arguments.
14
+ */
15
+ update: (trigger: HTMLElement, ancestors: HTMLElement[]) => void;
16
+ /**
17
+ * Elements whose own size changes must re-run `update`: a popup whose content changes size
18
+ * while open (a filtered list shrinking, an async list loading in) otherwise keeps coordinates
19
+ * computed for its old height and ends up detached from the trigger. Re-read on every update,
20
+ * since a panel can mount a commit later than the element it hangs off.
21
+ */
22
+ resizeRefs?: RefObject<HTMLElement | null>[];
23
+ /** Hands the popup's elements back as they were found. Runs when tracking stops. */
24
+ teardown?: () => void;
25
+ /** Values that must restart tracking, and so re-run `update`, when they change. */
26
+ deps?: readonly unknown[];
27
+ }
28
+ /**
29
+ * The lifecycle half of anchoring a portaled, `position: fixed` popup to a trigger: waits for the
30
+ * elements to mount, resolves the trigger's scrolling ancestors once, and re-runs the caller's
31
+ * positioning on every scroll, viewport resize and tracked size change until it closes.
32
+ *
33
+ * Callers supply only the geometry (`update`) and the inline styles to clear again (`teardown`);
34
+ * `useFloatingPosition` and `usePopoverPosition` are both built on this.
35
+ *
36
+ * Position is deliberately written straight to the DOM by the caller rather than through React
37
+ * state: going through setState -> re-render -> commit adds a round-trip that lags behind the
38
+ * browser's own scroll painting by at least a frame.
39
+ */
40
+ export declare function useAnchorTracking({ isOpen, triggerRef, elementRefs, update, resizeRefs, teardown, deps, }: UseAnchorTrackingArgs): void;
@@ -19,6 +19,12 @@ export interface InputDropdownProps extends Omit<HtmlProps, 'onSelect'> {
19
19
  * instead of only when an item is picked from the list.
20
20
  */
21
21
  allowCustomValue?: boolean;
22
+ /**
23
+ * Maximum number of filtered items rendered in the list. Matches beyond this
24
+ * are dropped (the user narrows by typing, not by scrolling). `0` renders all
25
+ * matches. Default `100`.
26
+ */
27
+ maxItems?: number;
22
28
  onSelect?: (value: string) => void;
23
29
  onChange?: (value: unknown) => void;
24
30
  placeholder?: string;
@@ -0,0 +1,2 @@
1
+ export { default } from './multiSelect';
2
+ export * from './multiSelect.types';
@@ -0,0 +1,4 @@
1
+ import { ReactElement } from 'react';
2
+ import { MultiSelectProps } from './multiSelect.types';
3
+ declare function MultiSelect(props: MultiSelectProps): ReactElement;
4
+ export default MultiSelect;
@@ -0,0 +1,62 @@
1
+ import { CSSProperties } from 'react';
2
+ import { FormValidator } from '../../services/formValidation';
3
+ import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
4
+ import { Presentation } from '../floatingMenu/floatingMenu.types';
5
+ export type MultiSelectValue = string | number;
6
+ export interface MultiSelectItem {
7
+ key: MultiSelectValue;
8
+ /** Visible text. A string, not a `ReactNode`, because it also renders inside a `Chip`. */
9
+ label: string;
10
+ /** Disable just this option. */
11
+ disabled?: boolean;
12
+ }
13
+ export type MultiSelectSlots = 'root' | 'label' | 'fieldset' | 'control' | 'values' | 'chip' | 'placeholder' | 'clearButton' | 'icon' | 'panel' | 'search' | 'options' | 'option' | 'optionCheck' | 'optionLabel' | 'empty';
14
+ export interface MultiSelectProps extends Omit<HtmlProps<HTMLDivElement>, 'onChange' | 'value' | 'defaultValue'> {
15
+ /** The selectable options. */
16
+ items: MultiSelectItem[];
17
+ /** Controlled selection. Omit for uncontrolled. */
18
+ value?: MultiSelectValue[];
19
+ /** Initial selection when uncontrolled (default `[]`). */
20
+ defaultValue?: MultiSelectValue[];
21
+ /** Fires with the full new selection, in the order the items were picked. */
22
+ onChange?: (value: MultiSelectValue[]) => void;
23
+ /** Field label. Rests inside the control and floats up into the border notch, like `Input`. */
24
+ label?: string;
25
+ /** Keep the label floating even when the field is empty and unfocused (default `false`). */
26
+ alwaysFloatLabel?: boolean;
27
+ /** Shown while nothing is selected. With a `label`, only once the label has floated clear. */
28
+ placeholder?: string;
29
+ /** Show a search box in the panel that filters the options (default `false`). */
30
+ searchable?: boolean;
31
+ /**
32
+ * Let the user add a value that isn't in `items` by typing it and picking the "add" row that
33
+ * appears. Implies `searchable`, since the search box is where the value is typed. The new
34
+ * value becomes its own key, so `onChange` reports it as a plain string.
35
+ */
36
+ allowCustomValues?: boolean;
37
+ /** Label for the row that adds a typed value (default `Add "…"`). */
38
+ createLabel?: (value: string) => string;
39
+ /** Placeholder for the search box (default `'Search…'`). */
40
+ searchPlaceholder?: string;
41
+ /** Text shown in the panel when no option matches (default `'No matches'`). */
42
+ emptyLabel?: string;
43
+ /** Cap the number of selections. Unselected options disable once the cap is reached. */
44
+ maxSelected?: number;
45
+ /** Show a button that clears the whole selection (default `true`). */
46
+ clearable?: boolean;
47
+ disabled?: boolean;
48
+ /** Show the selection but allow no changes, without the dimming `disabled` applies. */
49
+ readOnly?: boolean;
50
+ /** Where the panel opens: anchored to the control, as a bottom sheet, or a sheet only on a
51
+ * small touch screen. Forwarded to `Popover`. */
52
+ presentation?: Presentation;
53
+ formValidator?: FormValidator | null;
54
+ /** Manual error message shown in a tooltip (used when there's no `formValidator`). */
55
+ errorMessage?: string;
56
+ /** Manual valid state when not using a `formValidator` (default `true`). */
57
+ isValid?: boolean;
58
+ className?: string;
59
+ style?: CSSProperties;
60
+ classNames?: SlotClassNames<MultiSelectSlots>;
61
+ styles?: SlotStyles<MultiSelectSlots>;
62
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from './pagination';
2
+ export * from './pagination.types';
@@ -0,0 +1,4 @@
1
+ import { ReactElement } from 'react';
2
+ import { PaginationProps } from './pagination.types';
3
+ declare function Pagination(props: PaginationProps): ReactElement | null;
4
+ export default Pagination;
@@ -0,0 +1,30 @@
1
+ import { CSSProperties } from 'react';
2
+ import { IconContent } from '../types/icon.types';
3
+ import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
4
+ export type PaginationSlots = 'root' | 'item' | 'control' | 'ellipsis';
5
+ export interface PaginationProps extends Omit<HtmlProps<HTMLElement>, 'onChange'> {
6
+ /** Total number of pages. Nothing renders below `1`. */
7
+ total: number;
8
+ /** Controlled current page, 1-based (omit for uncontrolled). */
9
+ page?: number;
10
+ /** Initial page when uncontrolled (default `1`). */
11
+ defaultPage?: number;
12
+ onChange?: (page: number) => void;
13
+ /** Pages shown either side of the current one (default `1`). */
14
+ siblings?: number;
15
+ /** Pages always shown at the start and end (default `1`). */
16
+ boundaries?: number;
17
+ /** Show the previous/next controls (default `true`). */
18
+ showControls?: boolean;
19
+ /** Show first/last jump controls outside the previous/next ones (default `false`). */
20
+ showEdges?: boolean;
21
+ disabled?: boolean;
22
+ previousIcon?: IconContent;
23
+ nextIcon?: IconContent;
24
+ firstIcon?: IconContent;
25
+ lastIcon?: IconContent;
26
+ className?: string;
27
+ style?: CSSProperties;
28
+ classNames?: SlotClassNames<PaginationSlots>;
29
+ styles?: SlotStyles<PaginationSlots>;
30
+ }
@@ -17,7 +17,9 @@ export interface UsePopoverPositionArgs {
17
17
  * Positions a portaled, `position: fixed` popover on any of the four sides of its trigger with
18
18
  * start/center/end cross-axis alignment. Flips to the opposite side when the preferred one has no
19
19
  * room, shifts back on-screen along the cross axis, points the arrow at the trigger centre, and
20
- * hides (not closes) the panel once the trigger scrolls behind a clipping ancestor. Position is
21
- * written straight to the DOM via refs — going through setState lags the browser's scroll paint.
20
+ * hides (not closes) the panel once the trigger scrolls behind a clipping ancestor.
21
+ *
22
+ * `useAnchorTracking` supplies the lifecycle around this: mount wait, ancestor resolution, and
23
+ * re-running on scroll, viewport resize and panel size changes.
22
24
  */
23
25
  export declare function usePopoverPosition({ isOpen, placement, align, offset, triggerRef, floatingRef, panelRef, arrowRef, }: UsePopoverPositionArgs): void;
@@ -0,0 +1,3 @@
1
+ import { FlatTreeNode, TreeNode, TreeValue } from './tree.types';
2
+ /** The visible nodes in render order. Flat, not nested, deliberately: see docs/Tree.md. */
3
+ export declare function flattenTree(nodes: TreeNode[], expandedKeys: TreeValue[], level?: number, parentKey?: TreeValue | null): FlatTreeNode[];
@@ -0,0 +1,3 @@
1
+ export { default } from './tree';
2
+ export { flattenTree } from './flattenTree';
3
+ export * from './tree.types';
@@ -0,0 +1,4 @@
1
+ import { ReactElement } from 'react';
2
+ import { TreeProps } from './tree.types';
3
+ declare function Tree(props: TreeProps): ReactElement;
4
+ export default Tree;
@@ -0,0 +1,62 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { IconContent } from '../types/icon.types';
3
+ import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types';
4
+ export type TreeValue = string | number;
5
+ export interface TreeNode {
6
+ key: TreeValue;
7
+ label: ReactNode;
8
+ /** Icon shown before the label. */
9
+ icon?: IconContent;
10
+ /** Child nodes. An empty array still renders a toggle, for a branch whose children load later. */
11
+ children?: TreeNode[];
12
+ disabled?: boolean;
13
+ }
14
+ /** One visible row produced by `flattenTree`. */
15
+ export interface FlatTreeNode {
16
+ node: TreeNode;
17
+ /** Depth from the roots, 0-based. */
18
+ level: number;
19
+ parentKey: TreeValue | null;
20
+ isBranch: boolean;
21
+ isExpanded: boolean;
22
+ /** Number of siblings at this level, for `aria-setsize`. */
23
+ setSize: number;
24
+ /** 1-based position among those siblings, for `aria-posinset`. */
25
+ posInSet: number;
26
+ }
27
+ export type TreeSlots = 'root' | 'node' | 'toggle' | 'icon' | 'label' | 'empty';
28
+ export interface TreeProps extends Omit<HtmlProps<HTMLDivElement>, 'onSelect'> {
29
+ nodes: TreeNode[];
30
+ /** Controlled set of expanded branch keys. Omit for uncontrolled. */
31
+ expandedKeys?: TreeValue[];
32
+ /** Branches expanded initially when uncontrolled (default `[]`). */
33
+ defaultExpandedKeys?: TreeValue[];
34
+ onExpandedChange?: (keys: TreeValue[]) => void;
35
+ /** Controlled selected key. Omit for uncontrolled. */
36
+ selectedKey?: TreeValue | null;
37
+ /** Key selected initially when uncontrolled. */
38
+ defaultSelectedKey?: TreeValue;
39
+ onSelect?: (key: TreeValue, node: TreeNode) => void;
40
+ /** Clicking a branch's label toggles it as well as selecting it (default `true`). */
41
+ expandOnSelect?: boolean;
42
+ /** Indent per level in px (default `20`). */
43
+ indent?: number;
44
+ /** Icon for the expand/collapse toggle. Rotates 90° when open (default a right chevron). */
45
+ toggleIcon?: IconContent;
46
+ /**
47
+ * Sets a fixed viewport height and windows the rows through `VirtualList`, so only the visible
48
+ * ones are in the DOM. Use it past a few hundred visible nodes. Rows must then be uniform
49
+ * height: set `--tree-node-height` rather than sizing individual rows.
50
+ */
51
+ height?: number | string;
52
+ /** Row height used by the windowed renderer before measurement (default `32`). */
53
+ estimatedRowHeight?: number;
54
+ /** Shown when `nodes` is empty (default `'Nothing here'`). */
55
+ emptyLabel?: string;
56
+ disabled?: boolean;
57
+ 'aria-label'?: string;
58
+ className?: string;
59
+ style?: CSSProperties;
60
+ classNames?: SlotClassNames<TreeSlots>;
61
+ styles?: SlotStyles<TreeSlots>;
62
+ }
@@ -83,6 +83,14 @@ export interface VirtualListProps<T> extends HtmlProps {
83
83
  renderRow?: (item: T, index: number) => ReactNode;
84
84
  /** Column definitions — presence enables table/header mode. */
85
85
  columns?: VirtualListColumn<T>[];
86
+ /**
87
+ * Replaces the roles put on the container and rows (default `list`/`listitem`, or `grid`/`row`
88
+ * with columns). Pass `'none'` for both when the rows carry their own semantics, as `Tree` does.
89
+ */
90
+ ariaRoles?: {
91
+ container: string;
92
+ row: string;
93
+ };
86
94
  /** Height of the scroll viewport. Defaults to '100%' to fill the parent container. */
87
95
  height?: number | string;
88
96
  /** Estimated row height before measurement. Default: 40. */
@@ -14,7 +14,7 @@ export interface VirtualRowProps {
14
14
  ariaRowIndex?: number;
15
15
  ariaPosInSet?: number;
16
16
  ariaSetSize?: number;
17
- rowRole: 'row' | 'listitem';
17
+ rowRole: string;
18
18
  reorderable?: boolean;
19
19
  /**
20
20
  * Native mouse DnD is active for the list. Distinct from `draggable`, which in
@@ -75,6 +75,8 @@ export * from './common/klipyPicker';
75
75
  export * from './common/loading';
76
76
  export { default as Modal } from './common/modal';
77
77
  export * from './common/modal';
78
+ export { default as MultiSelect } from './common/multiSelect';
79
+ export * from './common/multiSelect';
78
80
  export { default as NumberInput } from './common/numberInput';
79
81
  export * from './common/numberInput';
80
82
  export { default as OptionPicker } from './common/optionPicker';
@@ -83,6 +85,8 @@ export { default as OtpInput } from './common/otpInput';
83
85
  export * from './common/otpInput';
84
86
  export { default as Overscroll } from './common/overscroll';
85
87
  export * from './common/overscroll';
88
+ export { default as Pagination } from './common/pagination';
89
+ export * from './common/pagination';
86
90
  export { default as Popover } from './common/popover';
87
91
  export * from './common/popover';
88
92
  export { default as ProgressBar } from './common/progressBar';
@@ -138,6 +142,8 @@ export { default as ToastProvider } from './common/toast';
138
142
  export * from './common/toast';
139
143
  export { default as Tooltip } from './common/tooltip';
140
144
  export * from './common/tooltip';
145
+ export { default as Tree } from './common/tree';
146
+ export * from './common/tree';
141
147
  export { default as VirtualList } from './common/virtualList';
142
148
  export * from './common/virtualList';
143
149
  export { default as Wizard } from './common/wizard';
package/docs/CLAUDE.md CHANGED
@@ -156,10 +156,12 @@ FontAwesome internals, so they still take an `IconDefinition` only.
156
156
  @KlipyPicker.md
157
157
  @Loading.md
158
158
  @Modal.md
159
+ @MultiSelect.md
159
160
  @NumberInput.md
160
161
  @OptionPicker.md
161
162
  @OtpInput.md
162
163
  @Overscroll.md
164
+ @Pagination.md
163
165
  @Popover.md
164
166
  @ProgressBar.md
165
167
  @RadioGroup.md
@@ -186,5 +188,6 @@ FontAwesome internals, so they still take an `IconDefinition` only.
186
188
  @Timer.md
187
189
  @Toast.md
188
190
  @Tooltip.md
191
+ @Tree.md
189
192
  @VirtualList.md
190
193
  @Wizard.md
@@ -58,6 +58,7 @@ interface InputDropdownItem {
58
58
  | `value` | `string` | Controlled value |
59
59
  | `items` | `InputDropdownItem[]` | Dropdown options |
60
60
  | `allowCustomValue` | `boolean` | Allow free-text values not in `items` — the typed text is emitted via `onChange` on every keystroke (default `false`) |
61
+ | `maxItems` | `number` | Maximum number of filtered matches rendered in the list. Further matches are dropped, since the list is narrowed by typing rather than by scrolling. `0` renders all matches. Default `100` |
61
62
  | `onSelect` | `(value: string) => void` | Called when an item is picked from the list |
62
63
  | `onChange` | `(value: unknown) => void` | Called on every text change |
63
64
  | `placeholder` | `string` | |
@@ -68,4 +69,6 @@ interface InputDropdownItem {
68
69
 
69
70
  **Slots:** `root` `dropdown` `item`
70
71
 
72
+ Long lists are handled by capping how many matches are rendered (`maxItems`, default `100`) rather than by virtualizing: only about six rows fit in the scroll box, so anything past the cap is DOM the user never sees. Raise `maxItems` (or set it to `0`) if a list is genuinely meant to be scrolled end to end. For lists too large to pass as `items` at all, fetch on the server, keep the results in your own state and feed them in as `items`.
73
+
71
74
  The list is rendered through a portal and tracks the trigger across scroll, so it always escapes clipping ancestors (cards, scroll containers, virtualized lists). It flips to open upward automatically when there's no room below, and shifts horizontally to stay within the viewport when the trigger sits near the left or right edge of the screen. It hides (without closing) if the trigger itself scrolls behind a clipping ancestor, reappearing once it's back in view. It also re-measures when its own content changes size while open, so a list that shrinks (e.g. filtered down to fewer entries) stays anchored to the trigger instead of hanging above it.
@@ -0,0 +1,100 @@
1
+ # MultiSelect
2
+
3
+ **When to use:** Pick several options from a list. Selections appear as removable chips in the control, and the list opens in a panel below it. For a single choice use [Dropdown](Dropdown.md); for a handful of options that should stay visible use [OptionPicker](OptionPicker.md) or [Checkbox](Checkbox.md); for type-ahead against one value use [InputDropdown](InputDropdown.md).
4
+
5
+ **Keywords:** tags, token input, multi-value, pick many, select multiple, facet, chips picker
6
+
7
+ **Import:** `import { MultiSelect } from '@ahrowe/ui'`
8
+ **Types:** `import type { MultiSelectProps, MultiSelectItem, MultiSelectValue } from '@ahrowe/ui'`
9
+
10
+ **Requires:** `<div id="bodyEnd"></div>` in your app — the panel renders through a portal.
11
+
12
+ ```tsx
13
+ import { useState } from 'react';
14
+ import { MultiSelect } from '@ahrowe/ui';
15
+ import type { MultiSelectItem, MultiSelectValue } from '@ahrowe/ui';
16
+
17
+ const items: MultiSelectItem[] = [
18
+ { key: 'apple', label: 'Apple' },
19
+ { key: 'banana', label: 'Banana' },
20
+ { key: 'fig', label: 'Fig', disabled: true },
21
+ ];
22
+
23
+ // Controlled
24
+ const [value, setValue] = useState<MultiSelectValue[]>([]);
25
+ <MultiSelect items={items} value={value} onChange={setValue} label="Fruit" placeholder="Pick some" />
26
+
27
+ // Uncontrolled
28
+ <MultiSelect items={items} defaultValue={['apple']} onChange={(value) => console.log(value)} />
29
+
30
+ // Searchable, for a list too long to scan
31
+ <MultiSelect items={countries} searchable searchPlaceholder="Filter countries…" />
32
+
33
+ // Tag entry: type a value that isn't in `items` and pick the "add" row
34
+ <MultiSelect items={commonTags} allowCustomValues createLabel={(value) => `Neu: ${value}`} />
35
+
36
+ // Cap the selection — unselected options disable once the cap is reached
37
+ <MultiSelect items={items} maxSelected={3} />
38
+
39
+ // Connected to a FormValidator, like Input and Dropdown
40
+ <MultiSelect items={items} formValidator={form.fruit} label="Fruit" />
41
+
42
+ // Manual error state, when there's no validator
43
+ <MultiSelect items={items} isValid={false} errorMessage="Pick at least one" />
44
+
45
+ // Always a bottom sheet, rather than only on a small touch screen
46
+ import { Presentation } from '@ahrowe/ui';
47
+ <MultiSelect items={items} presentation={Presentation.Sheet} />
48
+ ```
49
+
50
+ **Value:** `onChange` gets the full new selection as an array of `key`s, in the order the user picked them, and the chips render in that same order. It is never called with a partial change, so `onChange={setValue}` is all a controlled field needs.
51
+
52
+ **The label floats like `Input`'s.** It rests vertically centred over the control and rises into a notch cut out of the border once the field is focused, open, or has a selection, using the same `fieldset`/`legend` construction and the same `--input-notch-height` geometry as `Input`. A `MultiSelect` sitting next to an `Input` in a form animates with it rather than against it. Set `alwaysFloatLabel` to pin it up, which is what you want when a `placeholder` should always be readable: with a label present the placeholder is otherwise faded in on a delay matching the label's animation, so the two never sit on top of each other. Without a label there is nothing to wait for and it fades in at once.
53
+
54
+ **Errors:** with a `formValidator` the field stays quiet until it has been touched (the first time the panel closes), then shows the validator's message while the field is hovered or focused, and drops it as soon as the validator is happy again. Without a validator, `isValid` and `errorMessage` are plain props and nothing clears them for you: derive them from your own state, as in `isValid={picked.length > 0}`.
55
+
56
+ **Height:** a MultiSelect with one row of chips is exactly as tall as an `Input` or `Dropdown` beside it, so a form row lines up. It grows only when the chips wrap onto a second row. The chips render at `0.8em` to fit: a standalone `Chip` is nearly as tall as the whole field, because its remove button carries `ActionIcon`'s padding around a `1em` icon.
57
+
58
+ **Custom values** (`allowCustomValues`) turn this into a tag input. Anything typed that matches no item's label or key, and isn't already selected, gets an extra row at the bottom of the panel offering to add it; picking that row commits the typed string as its own key and clears the box. It goes through the same highlight and `Enter` path as a normal option, so the keyboard works unchanged, and `maxSelected` still caps it. Because a custom value has no entry in `items`, its chip falls back to showing the value itself. Without `allowCustomValues`, a selected key that isn't in `items` renders no chip at all.
59
+
60
+ **Item labels are strings, not nodes.** The same text renders twice, in the option row and inside the `Chip`, and `Chip` takes a string. Reach for `Dropdown` when an option needs rich content.
61
+
62
+ **Key props:**
63
+
64
+ | Prop | Type | Description |
65
+ |------|------|-------------|
66
+ | `items` | `MultiSelectItem[]` | The options: `{ key, label, disabled? }` |
67
+ | `value` | `MultiSelectValue[]` | Controlled selection (omit for uncontrolled) |
68
+ | `defaultValue` | `MultiSelectValue[]` | Initial selection when uncontrolled (default `[]`) |
69
+ | `onChange` | `(value: MultiSelectValue[]) => void` | Fires with the full new selection |
70
+ | `label` | `string` | Field label. Rests inside the control, floats into the border notch |
71
+ | `alwaysFloatLabel` | `boolean` | Keep the label floating even when empty and unfocused (default `false`) |
72
+ | `placeholder` | `string` | Shown while nothing is selected. With a `label`, it fades in only after the label has finished floating clear |
73
+ | `searchable` | `boolean` | Show a search box in the panel (default `false`) |
74
+ | `allowCustomValues` | `boolean` | Let the user add a value not in `items`. Implies `searchable` |
75
+ | `createLabel` | `(value: string) => string` | Label for the add row (default `Add "…"`) |
76
+ | `searchPlaceholder` | `string` | Placeholder for that box (default `'Search…'`) |
77
+ | `emptyLabel` | `string` | Shown when nothing matches (default `'No matches'`) |
78
+ | `maxSelected` | `number` | Cap the selection; unselected options disable at the cap |
79
+ | `clearable` | `boolean` | Show the clear-all button (default `true`) |
80
+ | `disabled` | `boolean` | Dims the field and blocks interaction |
81
+ | `readOnly` | `boolean` | Shows the selection, allows no changes, no dimming |
82
+ | `presentation` | `Presentation` | Anchored panel, bottom sheet, or sheet only on small touch screens |
83
+ | `formValidator` | `FormValidator \| null` | Owns the value when present. See [FormValidator.md](FormValidator.md) |
84
+ | `errorMessage` | `string` | Manual error, shown in a tooltip when there's no validator |
85
+ | `isValid` | `boolean` | Manual valid state (default `true`) |
86
+
87
+ **Keyboard:** `↓` or `Enter` opens the panel. `↑`/`↓` move the highlight and wrap around, `Home`/`End` jump to the ends, `Enter` toggles the highlighted option, `Escape` closes. `Backspace` removes the last selection when the search box is empty. With `searchable`, focus moves into the search box on open and the same keys work from there.
88
+
89
+ **Accessibility:** the control is a `role="combobox"` with `aria-expanded`, `aria-haspopup="listbox"` and `aria-controls`; the panel is a `role="listbox"` with `aria-multiselectable="true"`, and each option carries `aria-selected`. The highlighted option is reported through `aria-activedescendant`, so focus stays on the control (or the search box) rather than moving between options. Each chip's remove button is labelled `Remove <label>`.
90
+
91
+ **Theming:** override these CSS variables theme-wide via `ThemeProvider` or per instance via `style`; each falls back to a built-in default:
92
+
93
+ | Variable | Falls back to |
94
+ |----------|---------------|
95
+ | `--multi-select-min-height` | `calc(1.25em + 14px)`, the same height `Input` and `Dropdown` resolve to |
96
+ | `--multi-select-panel-height` | `260px` |
97
+
98
+ **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ MultiSelect: { searchable: true } }}`. See [ConfigProvider.md](ConfigProvider.md).
99
+
100
+ **Slots:** `root` `label` `fieldset` `control` `values` `chip` `placeholder` `clearButton` `icon` `panel` `search` `options` `option` `optionCheck` `optionLabel` `empty`
@@ -0,0 +1,72 @@
1
+ # Pagination
2
+
3
+ **When to use:** Move through server-paged results a page at a time — a results list, an admin table, a report. For endless scrolling instead, use `InfiniteBlock`; for a long list that's all in memory, use `VirtualList`.
4
+
5
+ **Keywords:** pager, paging, page numbers, next page, previous page, page navigation, offset, results
6
+
7
+ **Import:** `import { Pagination } from '@ahrowe/ui'`
8
+ **Types:** `import type { PaginationProps } from '@ahrowe/ui'`
9
+
10
+ ```tsx
11
+ import { useState } from 'react';
12
+ import { Pagination } from '@ahrowe/ui';
13
+
14
+ // Controlled — the usual shape, since the page drives a fetch
15
+ const [page, setPage] = useState(1);
16
+ <Pagination total={20} page={page} onChange={setPage} />
17
+
18
+ // Uncontrolled
19
+ <Pagination total={20} defaultPage={3} onChange={(page) => console.log(page)} />
20
+
21
+ // Total pages from a row count
22
+ <Pagination total={Math.ceil(rowCount / pageSize)} page={page} onChange={setPage} />
23
+
24
+ // First/last jump controls outside the previous/next ones
25
+ <Pagination total={50} page={page} onChange={setPage} showEdges />
26
+
27
+ // Page numbers only, no previous/next
28
+ <Pagination total={20} page={page} onChange={setPage} showControls={false} />
29
+
30
+ // Narrower — one page either side of the current one is already the default, `0` shows just the current
31
+ <Pagination total={100} page={page} onChange={setPage} siblings={0} />
32
+
33
+ // Wider window and more pages pinned at each end
34
+ <Pagination total={100} page={page} onChange={setPage} siblings={2} boundaries={2} />
35
+
36
+ // Disabled while the page's data is loading
37
+ <Pagination total={20} page={page} onChange={setPage} disabled={isLoading} />
38
+ ```
39
+
40
+ **Which pages are shown:** the first and last `boundaries` pages, the current page with `siblings` pages either side, and a `…` wherever a run was collapsed. A gap only appears when it replaces more than one page — collapsing a single page would take the same width and hide a reachable page, so that side lists a fixed-size block instead. With the defaults (`siblings={1}`, `boundaries={1}`) the control is always 7 page slots wide, so it never reflows as the user pages through.
41
+
42
+ **Key props:**
43
+
44
+ | Prop | Type | Description |
45
+ |------|------|-------------|
46
+ | `total` | `number` | Total number of pages (not rows). Renders nothing below `1` |
47
+ | `page` | `number` | Controlled current page, 1-based (omit for uncontrolled). Clamped to `1…total` |
48
+ | `defaultPage` | `number` | Initial page when uncontrolled (default `1`) |
49
+ | `onChange` | `(page: number) => void` | Fires with the new page; not called when the current page is clicked again |
50
+ | `siblings` | `number` | Pages shown either side of the current one (default `1`) |
51
+ | `boundaries` | `number` | Pages always shown at the start and end (default `1`) |
52
+ | `showControls` | `boolean` | Show the previous/next controls (default `true`) |
53
+ | `showEdges` | `boolean` | Show first/last jump controls outside the previous/next ones (default `false`) |
54
+ | `disabled` | `boolean` | Disables every button |
55
+ | `previousIcon` / `nextIcon` | `IconDefinition \| ReactElement` | Override the previous/next icons (default chevrons) |
56
+ | `firstIcon` / `lastIcon` | `IconDefinition \| ReactElement` | Override the first/last icons (default double chevrons) |
57
+ | `aria-label` | `string` | Accessible name for the `<nav>` (default `'Pagination'`) |
58
+
59
+ **Accessibility:** renders a `<nav>` of `<button>`s. The current page carries `aria-current="page"`; each page button is labelled `Go to page N`, the controls `Go to previous page` / `Go to next page` / `Go to first page` / `Go to last page`. Previous/next are disabled at the respective end rather than hidden, so the control doesn't reflow. The `…` is `aria-hidden`.
60
+
61
+ **Theming:** override these CSS variables theme-wide via `ThemeProvider` or per instance via `style`; each falls back to a built-in default:
62
+
63
+ | Variable | Falls back to |
64
+ |----------|---------------|
65
+ | `--pagination-active-background` | `var(--primary-color)` |
66
+ | `--pagination-active-color` | `var(--text-on-primary)` |
67
+ | `--pagination-size` | `36px` |
68
+ | `--pagination-gap` | `4px` |
69
+
70
+ **Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Pagination: { showEdges: true } }}`. See [ConfigProvider.md](ConfigProvider.md).
71
+
72
+ **Slots:** `root` `item` `control` `ellipsis`