@ahrowe/ui 0.45.0 → 0.47.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 (35) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cjs-types/common/input/input.types.d.ts +4 -3
  3. package/dist/cjs-types/common/kanbanBoard/KanbanBoard.d.ts +1 -1
  4. package/dist/cjs-types/common/kanbanBoard/KanbanColumn.d.ts +21 -12
  5. package/dist/cjs-types/common/kanbanBoard/KanbanItem.d.ts +1 -1
  6. package/dist/cjs-types/common/kanbanBoard/kanbanBoard.types.d.ts +23 -17
  7. package/dist/cjs-types/common/kanbanBoard/kanbanBoard.utils.d.ts +47 -1
  8. package/dist/esm/common/input/input.mjs +1 -1
  9. package/dist/esm/common/input/input.mjs.map +1 -1
  10. package/dist/esm/common/input/input.module.mjs +1 -1
  11. package/dist/esm/common/input/input.module.mjs.map +1 -1
  12. package/dist/esm/common/input/input.types.mjs.map +1 -1
  13. package/dist/esm/common/kanbanBoard/KanbanBoard.mjs +3 -2
  14. package/dist/esm/common/kanbanBoard/KanbanBoard.mjs.map +1 -1
  15. package/dist/esm/common/kanbanBoard/KanbanColumn.mjs +1 -1
  16. package/dist/esm/common/kanbanBoard/KanbanColumn.mjs.map +1 -1
  17. package/dist/esm/common/kanbanBoard/KanbanItem.mjs +1 -1
  18. package/dist/esm/common/kanbanBoard/KanbanItem.mjs.map +1 -1
  19. package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs +1 -1
  20. package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs.map +1 -1
  21. package/dist/esm/common/kanbanBoard/kanbanBoard.utils.mjs +1 -1
  22. package/dist/esm/common/kanbanBoard/kanbanBoard.utils.mjs.map +1 -1
  23. package/dist/index.cjs +4 -3
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/style.css +1 -1
  26. package/dist/types/common/input/input.types.d.ts +4 -3
  27. package/dist/types/common/kanbanBoard/KanbanBoard.d.ts +1 -1
  28. package/dist/types/common/kanbanBoard/KanbanColumn.d.ts +21 -12
  29. package/dist/types/common/kanbanBoard/KanbanItem.d.ts +1 -1
  30. package/dist/types/common/kanbanBoard/kanbanBoard.types.d.ts +23 -17
  31. package/dist/types/common/kanbanBoard/kanbanBoard.utils.d.ts +47 -1
  32. package/docs/Input.md +27 -9
  33. package/docs/KanbanBoard.md +36 -18
  34. package/docs/Textarea.md +1 -1
  35. package/package.json +1 -1
@@ -54,8 +54,10 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
54
54
  disabled?: boolean;
55
55
  upperRightLabel?: string | React.ReactNode;
56
56
  labelInBorder?: boolean;
57
- showCancel?: boolean;
58
- onClearClicked?: (() => void) | null;
57
+ /** Show a button that clears the field, whenever it has a value it can change. */
58
+ clearable?: boolean;
59
+ /** Called after a clear button empties the field, by click or key. */
60
+ onClear?: (() => void) | null;
59
61
  min?: string | number | null;
60
62
  max?: string | number | null;
61
63
  placeholder?: string;
@@ -66,7 +68,6 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
66
68
  readOnly?: boolean;
67
69
  customInput?: React.ReactElement | null;
68
70
  autoFocus?: boolean;
69
- alwaysShowCancel?: boolean;
70
71
  formValidator?: FormValidator | null;
71
72
  multiline?: boolean;
72
73
  rows?: number;
@@ -1,3 +1,3 @@
1
1
  import { KanbanBoardProps } from './kanbanBoard.types.js';
2
- declare function KanbanBoard<T = unknown>({ columns, items, renderItem, onChange, dragHandle, getItemLabel, getItemDisabled, renderEmptyColumn, renderColumnActions, reorderableColumns, collapseEmptyColumns, columnMaxHeight, onLoadMore, loadMoreThreshold, className, style, classNames, styles: slotStyles, ...rest }: KanbanBoardProps<T>): import("react").JSX.Element;
2
+ declare function KanbanBoard<T = unknown>({ columns, items, renderItem, onChange, dragHandle, getItemLabel, getItemDisabled, renderEmptyColumn, renderColumnActions, reorderableColumns, collapseEmptyColumns, columnMaxHeight, onLoadMore, loadMoreThreshold, onKeyDown, className, style, classNames, styles: slotStyles, ...rest }: KanbanBoardProps<T>): import("react").JSX.Element;
3
3
  export default KanbanBoard;
@@ -1,25 +1,21 @@
1
- import { ReactNode } from 'react';
2
- import { KanbanColumnProps } from './kanbanBoard.types.js';
1
+ import { HTMLAttributes, ReactNode, Ref } from 'react';
2
+ import { DataProps, SlotClassNames, SlotStyles } from '../types/slots.types.js';
3
+ import { KanbanColumnProps, KanbanColumnSlots } from './kanbanBoard.types.js';
3
4
  interface KanbanColumnInternalProps<T> extends KanbanColumnProps {
4
5
  itemIds: string[];
5
6
  items: Record<string, T>;
6
7
  renderItem: (item: T, id: string) => ReactNode;
7
8
  dragHandle?: boolean;
8
- /**
9
- * Keep the column at its normal flex width even when `itemIds` is empty. Used while a drag
10
- * is in progress: the empty-column collapse is driven by transient shadow state during a
11
- * drag, and letting the column shrink mid-drag shifts every column's layout underneath the
12
- * pointer, which can flip the collision target back and forth forever.
13
- */
14
- preserveSizeWhileEmpty?: boolean;
15
- /** Shrink to a narrow strip when empty — see `KanbanBoardProps.collapseEmptyColumns`. Defaults to `true`. */
16
- collapseEmptyColumns?: boolean;
9
+ /** Shrink to a narrow strip. The board decides when, see `KanbanBoardProps.collapseEmptyColumns`. */
10
+ collapsed?: boolean;
17
11
  /** Work-in-progress limit — see `KanbanColumnDef.maxItems`. Display-only. */
18
12
  maxItems?: number;
19
13
  /** Freezes the column — see `KanbanColumnDef.disabled`. */
20
14
  disabled?: boolean;
21
15
  /** Returns whether a specific card can't be picked up — see `KanbanBoardProps.getItemDisabled`. */
22
16
  getItemDisabled?: (item: T, id: string) => boolean;
17
+ /** Names a card's drag handle — see `KanbanBoardProps.getItemLabel`. */
18
+ getItemLabel?: (item: T, id: string) => string;
23
19
  /** Custom content shown when the column has no cards — see `KanbanBoardProps.renderEmptyColumn`. */
24
20
  emptyPlaceholder?: ReactNode;
25
21
  /** Custom header actions — see `KanbanBoardProps.renderColumnActions`. */
@@ -41,5 +37,18 @@ interface KanbanColumnInternalProps<T> extends KanbanColumnProps {
41
37
  /** Scroll distance from the bottom that triggers `onLoadMore` — see `KanbanBoardProps.loadMoreThreshold`. */
42
38
  loadMoreThreshold?: number;
43
39
  }
44
- declare function KanbanColumn<T>({ id, title, itemIds, items, renderItem, dragHandle, preserveSizeWhileEmpty, collapseEmptyColumns, maxItems, disabled, getItemDisabled, emptyPlaceholder, actions, reorderable, activeIsColumn, columnMaxHeight, hasMore, onLoadMore, loadMoreThreshold, className, style, classNames, styles: slotStyles, ...rest }: KanbanColumnInternalProps<T>): import("react").JSX.Element;
40
+ interface ColumnHeaderContentProps {
41
+ title: string;
42
+ count: number;
43
+ maxItems?: number;
44
+ actions?: ReactNode;
45
+ /** Makes the title the column's keyboard drag handle. */
46
+ titleProps?: HTMLAttributes<HTMLSpanElement> & DataProps & {
47
+ ref?: Ref<HTMLSpanElement>;
48
+ };
49
+ classNames?: SlotClassNames<KanbanColumnSlots>;
50
+ styles?: SlotStyles<KanbanColumnSlots>;
51
+ }
52
+ export declare function ColumnHeaderContent({ title, count, maxItems, actions, titleProps, classNames, styles: slotStyles, }: ColumnHeaderContentProps): import("react").JSX.Element;
53
+ declare function KanbanColumn<T>({ id, title, itemIds, items, renderItem, dragHandle, collapsed, maxItems, disabled, getItemDisabled, getItemLabel, emptyPlaceholder, actions, reorderable, activeIsColumn, columnMaxHeight, hasMore, onLoadMore, loadMoreThreshold, className, style, classNames, styles: slotStyles, ...rest }: KanbanColumnInternalProps<T>): import("react").JSX.Element;
45
54
  export default KanbanColumn;
@@ -1,3 +1,3 @@
1
1
  import { KanbanItemProps } from './kanbanBoard.types.js';
2
- declare function KanbanItem({ id, dragHandle, disabled, preventAsDropTarget, children }: KanbanItemProps): import("react").JSX.Element;
2
+ declare function KanbanItem({ id, label, dragHandle, disabled, preventAsDropTarget, children }: KanbanItemProps): import("react").JSX.Element;
3
3
  export default KanbanItem;
@@ -38,14 +38,17 @@ export interface KanbanBoardProps<T = unknown> extends Omit<HtmlProps, 'onChange
38
38
  */
39
39
  onChange: (columns: KanbanColumnDef[]) => void;
40
40
  /**
41
- * Restrict drag initiation to elements marked `data-drag-handle` within a card,
42
- * instead of the whole card. Use when `renderItem` includes interactive content
43
- * (buttons, inputs) that would otherwise have its clicks swallowed by the drag listener.
41
+ * Drag a card only by the element marked `data-drag-handle` inside it, with the mouse, touch
42
+ * and the keyboard, instead of by the whole card. The handle is made focusable and named with
43
+ * `getItemLabel` unless it has a name, and the card stops being a button. Use when `renderItem`
44
+ * includes controls (buttons, inputs): inside a card that is itself the button, a screen reader
45
+ * reads them as plain text.
44
46
  */
45
47
  dragHandle?: boolean;
46
48
  /**
47
49
  * Produce an accessible label for a card, announced to screen-reader users while dragging
48
- * (e.g. "Picked up <label>."). Defaults to the card's id when omitted.
50
+ * (e.g. "Picked up <label>."), and naming a `dragHandle` handle that has no name of its own.
51
+ * Defaults to the card's id when omitted.
49
52
  */
50
53
  getItemLabel?: (item: T, id: string) => string;
51
54
  /**
@@ -70,27 +73,28 @@ export interface KanbanBoardProps<T = unknown> extends Omit<HtmlProps, 'onChange
70
73
  */
71
74
  reorderableColumns?: boolean;
72
75
  /**
73
- * Shrink a column to a narrow strip once it has no cards, instead of keeping it at the same
74
- * width as every other column. Defaults to `true`. Set `false` to keep every column the same
75
- * width regardless of card count. The collapsed width itself is set via the
76
- * `--kanban-empty-column-width` CSS variable (falls back to `140px`) — override it theme-wide
77
- * via `ThemeProvider`'s `variables`, or per board via `style`.
76
+ * Shrink a column to a narrow strip while it has no cards. Defaults to `false`. Widths hold
77
+ * still during a drag: a column the drag empties or fills changes width after the drop. The
78
+ * collapsed width is set via the `--kanban-empty-column-width` CSS variable (falls back to
79
+ * `140px`): override it theme-wide via `ThemeProvider`'s `variables`, or per board via `style`.
78
80
  */
79
81
  collapseEmptyColumns?: boolean;
80
82
  /**
81
- * Bounds each column's height (any CSS length, e.g. `'70vh'`, `'600px'`) and makes its card
82
- * list scroll independently once content overflows, instead of growing the whole page. Leave
83
- * unset for the default unbounded height. Required for `onLoadMore` to ever trigger, since an
84
- * unbounded column never scrolls.
83
+ * Caps each column's card list at a length (any CSS length, e.g. `'70vh'`, `'600px'`), so a
84
+ * long column scrolls on its own instead of growing the page. To fill the space your layout
85
+ * gives the board instead, give the board a height (`style={{ height: '100%' }}` in a parent
86
+ * with one): its columns then scroll the same way.
85
87
  */
86
88
  columnMaxHeight?: string;
87
89
  /**
88
90
  * Called with a column's `id` when that column has `hasMore` set and its card list is scrolled
89
- * within `loadMoreThreshold` of the bottom — fetch and append the next page of items for it.
91
+ * within `loadMoreThreshold` of the bottom: fetch and append the next page of items for it.
90
92
  * `KanbanColumn` tracks its own in-flight state per column (shown as a spinner at the bottom of
91
- * the list), so overlapping calls for the same column are never made concurrently. Also
92
- * retried automatically if a loaded batch doesn't fill the column's visible height, since a
93
- * column shorter than its own viewport never generates a scroll event to trigger a next page.
93
+ * the list), so overlapping calls for the same column are never made concurrently. Also called
94
+ * while the column's cards leave empty space below them, since a column that doesn't overflow
95
+ * can't be scrolled. Paging all the way needs a column that scrolls: `columnMaxHeight`, or a
96
+ * board with a height. On a board that grows with its cards, a short column only fills up to
97
+ * the tallest one.
94
98
  */
95
99
  onLoadMore?: (columnId: string) => void | Promise<void>;
96
100
  /**
@@ -115,6 +119,8 @@ export interface KanbanColumnProps extends HtmlProps {
115
119
  }
116
120
  export interface KanbanItemProps {
117
121
  id: string;
122
+ /** Names the card's `[data-drag-handle]` when it has no `aria-label` of its own. */
123
+ label?: string;
118
124
  dragHandle?: boolean;
119
125
  /** Whether this card can't be picked up (column frozen, or the card itself is pinned). */
120
126
  disabled?: boolean;
@@ -20,8 +20,28 @@ export declare function moveColumn(columns: KanbanColumnDef[], activeColumnId: s
20
20
  * which reads as needing to "wiggle it around" before the right drop target lights up. This
21
21
  * restricts the candidates to only the same kind as whatever is being dragged: a column drag only
22
22
  * considers other columns, a card drag only considers cards and card lists.
23
+ *
24
+ * A card drag also lets the pointer pick the column, and `closestCorners` only the position within
25
+ * it. Corners alone compare the dragged card's whole rect, so a wide card over a collapsed column
26
+ * had its corners nearer the neighbours' cards, and landed beside the column under the pointer.
27
+ * Without a pointer (the keyboard) or between columns, every column is a candidate as before.
23
28
  */
24
29
  export declare const kanbanCollisionDetection: CollisionDetection;
30
+ /**
31
+ * Whether the auto-scroll may move `scroller`, an ancestor of the board such as the page, while
32
+ * something is dragged: only toward a part of the board that is out of view. Nothing beyond the
33
+ * board takes a drop, and a page left free to scroll carried the whole board off screen.
34
+ */
35
+ export declare function scrollsTowardBoard(scroller: {
36
+ top: number;
37
+ bottom: number;
38
+ }, board: {
39
+ top: number;
40
+ bottom: number;
41
+ }, dragged: {
42
+ top: number;
43
+ height: number;
44
+ }): boolean;
25
45
  /** Extracts the column id from a column droppable's `column-<id>` identifier, or `null` for a card id. */
26
46
  export declare function resolveColumnDropId(overId: string): string | null;
27
47
  export interface DragGeometry {
@@ -40,9 +60,35 @@ export interface DragGeometry {
40
60
  * position (see the two branches below for what each one decides).
41
61
  */
42
62
  export declare function moveItem(columns: KanbanColumnDef[], activeItemId: string, overId: string, geometry: DragGeometry): KanbanColumnDef[];
63
+ /**
64
+ * Where a drag has put the dragged card or column so far. The board keeps this rather than a copy
65
+ * of `columns`, so a change the consumer makes mid-drag (a page appended by `onLoadMore`) still
66
+ * shows, and is still there in what `onChange` receives on drop.
67
+ */
68
+ export interface DragPlacement {
69
+ /** The dragged card's or column's id. */
70
+ id: string;
71
+ /** The column the card sits in, or `null` while a whole column is dragged. */
72
+ columnId: string | null;
73
+ index: number;
74
+ }
75
+ /** The position `id` holds in `columns`, as a placement to lay over a later `columns`. */
76
+ export declare function findPlacement(columns: KanbanColumnDef[], id: string, isColumn: boolean): DragPlacement | null;
77
+ /**
78
+ * Moves the dragged card or column to its placement in `columns`. Returns `columns` itself when
79
+ * there is no placement, or when the card, the column, or its destination is no longer there.
80
+ */
81
+ export declare function applyPlacement(columns: KanbanColumnDef[], placement: DragPlacement | null): KanbanColumnDef[];
82
+ /**
83
+ * Where an arrow key moves focus from a focused card or column handle while nothing is picked up.
84
+ * It mirrors what the same keys do to a picked-up card: Up/Down within the column, Left/Right to
85
+ * the nearest card in the next column with anything to focus, or from a handle to the next handle.
86
+ * Returns `null` when `from` is not a card or handle, or there is nowhere to go.
87
+ */
88
+ export declare function findArrowFocusTarget(board: HTMLElement, from: HTMLElement, key: string): HTMLElement | null;
43
89
  /**
44
90
  * Builds screen-reader drag announcements against the columns layout as currently understood
45
- * (the live shadow layout while a drag is in progress, or the settled `columns` prop otherwise).
91
+ * (the live layout while a drag is in progress, or the settled `columns` prop otherwise).
46
92
  */
47
93
  export declare function buildAnnouncements<T>(columns: KanbanColumnDef[], items: Record<string, T>, getItemLabel: (item: T, id: string) => string): Announcements;
48
94
  /**
package/docs/Input.md CHANGED
@@ -20,7 +20,7 @@ const emailValidator = new FormValidator('', [Validators.required(), Validators.
20
20
  <Input label="Password" type={InputType.Password} value={pw} onChange={setPw} />
21
21
 
22
22
  // Search — clear button appears automatically once there's a value, and clears itself;
23
- // onClearClicked is optional, only needed to react to the clear beyond the value itself
23
+ // onClear is optional, only needed to react to the clear beyond the value itself
24
24
  <Input
25
25
  type={InputType.Search}
26
26
  label="Search"
@@ -40,6 +40,9 @@ const emailValidator = new FormValidator('', [Validators.required(), Validators.
40
40
  // With suffix
41
41
  <Input label="Amount" suffix="€" value={amount} onChange={setAmount} />
42
42
 
43
+ // Clear button, shown whenever there is a value to clear
44
+ <Input label="City" clearable value={city} onChange={setCity} />
45
+
43
46
  // Row layout
44
47
  <Input label="Name" isRow value={name} onChange={setName} />
45
48
 
@@ -66,9 +69,9 @@ const emailValidator = new FormValidator('', [Validators.required(), Validators.
66
69
 
67
70
  **Custom icon vs. built-in type icon:** passing `icon` alongside a decorative type (`Edit`, `Phone`, `Email`) replaces that type's built-in icon outright, since there's nothing functional attached to it to lose. `Search` and `Password` ignore `icon` — their icon is functional (clear button, show/hide toggle), not decorative, so it always renders regardless of whether `icon` is set.
68
71
 
69
- **Search clear icon:** `InputType.Search` shows a magnifying glass that animates into a clear (×) button once the field has a value, rather than the browser's own native search-cancel decoration, which is suppressed so it doesn't clash with the theme.
72
+ **Search clear icon:** `InputType.Search` shows a magnifying glass that animates into a clear (×) button once the field has a value it can change, rather than the browser's own native search-cancel decoration, which is suppressed so it doesn't clash with the theme.
70
73
 
71
- **Icon accessibility:** any icon with an active click handler (`onIconClick`, the search clear button, the password show/hide toggle, the cancel/clear button) is keyboard-focusable (`Tab`) and activates on Enter or Space, with a visible focus outline. The ones with a fixed meaning (search clear, password toggle, cancel) also carry an `aria-label`. A purely decorative icon (no click handler) stays out of the tab order.
74
+ **Icon accessibility:** any icon with an active click handler (`onIconClick`, the search clear button, the password show/hide toggle, the `clearable` button) is keyboard-focusable (`Tab`) and activates on Enter or Space, with a visible focus outline. The ones with a fixed meaning (search clear, password toggle, clear) also carry an `aria-label`. A purely decorative icon (no click handler) stays out of the tab order. Either clear button puts the cursor back in the field, so a keyboard user does not lose their place.
72
75
 
73
76
  **Mobile keyboard layout:** `Email`, `Phone`, and `Search` each set the matching `inputMode` (`email` / `tel` / `search`), so mobile browsers show the right on-screen keyboard — an `@`-and-`.com` layout for email, a numeric dial pad for phone, a "Go"/search action key for search. `Email`'s native `type` is deliberately left as `text` rather than `email`, to avoid the browser's own "please match the requested format" validation UI fighting the `formValidator`-driven error tooltip this component already shows — `inputMode="email"` alone is enough to get the correct keyboard. Pass `inputMode` directly to override the computed default for any other case.
74
77
 
@@ -102,17 +105,16 @@ The stored value is the formatted one, spaces included, so strip them before a l
102
105
  | `isRequired` | `boolean` | |
103
106
  | `errorMessage` | `string` | Manual error (overrides formValidator) |
104
107
  | `isValid` | `boolean` | Manual valid state |
105
- | `multiline` | `boolean` | Renders as textarea |
108
+ | `multiline` | `boolean` | Renders as textarea, resizable from the field's corner; `styles={{ container: { resize: 'none' } }}` turns that off |
106
109
  | `rows` | `number` | Textarea row count |
107
110
  | `autoResize` | `boolean` | Textarea grows with content |
108
111
  | `suffix` | `string \| ReactNode` | Right-side suffix |
109
- | `icon` | `ReactElement` | Custom icon, positioned absolute on the side set by `iconPosition`; clickable via `onIconClick`. Replaces the built-in icon for `Edit`/`Phone`/`Email`; ignored for `Search`/`Password` (see below) |
112
+ | `icon` | `ReactElement` | Custom icon, on the side set by `iconPosition`; clickable via `onIconClick`. On the right it follows the `suffix` and the clear button, each in its own space. Replaces the built-in icon for `Edit`/`Phone`/`Email`; ignored for `Search`/`Password` (see below) |
110
113
  | `iconPosition` | `InputIconPosition` | Side the input's icon renders on — built-in or custom, whichever is showing (see enum above for the default). On the left, the material label (resting and floated) and the fieldset notch shift right to clear it too |
111
114
  | `onIconClick` | `() => void` | Click handler for the custom `icon` |
112
115
  | `upperRightLabel` | `string \| ReactNode` | Small label above right |
113
- | `showCancel` | `boolean` | Show clear icon when focused (ignored for `InputType.Search`, which always shows it once there's a value — see below) |
114
- | `alwaysShowCancel` | `boolean` | Always show clear icon |
115
- | `onClearClicked` | `() => void` | Called after the clear button clears the value. Optional — clicking clear always empties the field itself, whether or not this is set |
116
+ | `clearable` | `boolean` | Show a clear button whenever the field has a value it can change (not while `disabled` or `readOnly`). While the field is empty it takes no space, so a `suffix` sits at the edge; the first character grows it in. `InputType.Search` has its own, see below |
117
+ | `onClear` | `() => void` | Called after a clear button empties the field. Optional: clearing always empties the field itself |
116
118
  | `isRow` | `boolean` | Horizontal layout |
117
119
  | `labelInBorder` | `boolean` | Label inside fieldset border |
118
120
  | `alwaysFloatLabel` | `boolean` | Keep the material label floated in the border notch even when empty and unfocused, so an empty field reads as blank instead of showing the label as if it were a value. Only applies with `useMatLabelStyle` (default) |
@@ -138,4 +140,20 @@ The combobox components — `InputDropdown`, `SearchInput`, `MultiSelect`, `Date
138
140
 
139
141
  **Autofilled fields stay on the theme.** Chrome paints `:-webkit-autofill` with its own background and text colour, overriding `background` and `color` outright, which turned a filled field pale blue with near-black text on a dark theme. Input clips that background to the text instead of covering it: the page cannot win on colour, but it can say where a background paints, and `-webkit-text-fill-color` then beats the text colour. The field itself is transparent, so what shows through is whatever is genuinely behind it — inside a Modal, a Card, or a surface of your own — and nothing has to be told what that is.
140
142
 
141
- A custom `--input-background` is covered by the same mechanism, because the container paints it and the transparent field reveals it. Setting a background on the field itself with the `background` shorthand — `styles={{ input: { background: '…' } }}` — resets `background-clip` along with it, and an autofilled field there falls back to Chrome's own colours; `background-color` does not.
143
+ A custom `--input-background` is covered by the same mechanism, because the container paints it and the transparent field reveals it. Setting a background on the field itself with the `background` shorthand — `styles={{ input: { background: '…' } }}` — resets `background-clip` along with it, and an autofilled field there falls back to Chrome's own colours; `background-color` does not. Either way it covers only the text: a background meant for the whole field, under the icons and the suffix too, goes on `styles.container`.
144
+
145
+ ## Migration from `showCancel`
146
+
147
+ Before 0.47 the clear button came from two props, and its callback had a name that missed key
148
+ presses. TypeScript points at every use:
149
+
150
+ | Before | After |
151
+ |--------|-------|
152
+ | `showCancel` | `clearable` |
153
+ | `alwaysShowCancel` | `clearable` |
154
+ | `onClearClicked={…}` | `onClear={…}` |
155
+
156
+ What changes on screen: `showCancel` showed the button only on hover or focus, and
157
+ `alwaysShowCancel` showed it even on an empty field, where it did nothing. `clearable` shows it
158
+ whenever the field has a value, and never while it is `disabled` or `readOnly`, where both old
159
+ props would still clear it.
@@ -37,13 +37,14 @@ const items: Record<string, Task> = {
37
37
  onChange={(updatedColumns) => setColumns(updatedColumns)}
38
38
  />
39
39
 
40
- // Restrict drag initiation to a handle — needed when a card contains its own
41
- // interactive controls (buttons, inputs), which would otherwise have their
42
- // clicks swallowed by the drag listener on the whole card
40
+ // Drag by a handle instead of the whole card: needed when a card contains its own
41
+ // controls (buttons, inputs). The handle is also the card's keyboard handle, named
42
+ // with getItemLabel, so the card is not one button wrapped around the Delete button
43
43
  <KanbanBoard
44
44
  columns={columns}
45
45
  items={items}
46
46
  dragHandle
47
+ getItemLabel={(task) => task.title}
47
48
  renderItem={(task, id) => (
48
49
  <div className="task-card">
49
50
  <span data-drag-handle>⠿</span>
@@ -128,18 +129,33 @@ const columnsWithLock: KanbanColumnDef[] = [
128
129
  onChange={(updatedColumns) => setColumns(updatedColumns)}
129
130
  />
130
131
 
131
- // Keep every column the same width regardless of card count — opt out of the
132
- // default shrink-when-empty behaviour
132
+ // Shrink a column with no cards to a narrow strip. Widths hold still during a drag,
133
+ // so a column the drag empties or fills changes width once the card is dropped
133
134
  <KanbanBoard
134
135
  columns={columns}
135
136
  items={items}
136
137
  renderItem={(task) => <div className="task-card"><strong>{task.title}</strong></div>}
137
- collapseEmptyColumns={false}
138
+ collapseEmptyColumns
138
139
  onChange={(updatedColumns) => setColumns(updatedColumns)}
139
140
  />
140
141
 
141
- // Bound each column's height and let its card list scroll independently, instead
142
- // of growing the whole page — useful once a column can hold a lot of cards
142
+ // Fill the height your layout gives the board, like a full-screen board app: give the
143
+ // board a height and each column scrolls on its own inside it. Here the board takes
144
+ // what the header leaves of the window (minHeight: 0 lets a flex child shrink below
145
+ // its content)
146
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
147
+ <AppHeader />
148
+ <KanbanBoard
149
+ columns={columns}
150
+ items={items}
151
+ renderItem={(task) => <div className="task-card"><strong>{task.title}</strong></div>}
152
+ style={{ flex: 1, minHeight: 0 }}
153
+ onChange={(updatedColumns) => setColumns(updatedColumns)}
154
+ />
155
+ </div>
156
+
157
+ // Or cap each column at a length, so a long column scrolls on its own instead of
158
+ // growing the page, while a board of short columns stays short
143
159
  <KanbanBoard
144
160
  columns={columns}
145
161
  items={items}
@@ -148,12 +164,12 @@ const columnsWithLock: KanbanColumnDef[] = [
148
164
  onChange={(updatedColumns) => setColumns(updatedColumns)}
149
165
  />
150
166
 
151
- // Paginate a column instead of loading everything up front — mark it `hasMore`
167
+ // Paginate a column instead of loading everything up front: mark it `hasMore`
152
168
  // and fetch/append the next page in `onLoadMore` once it's scrolled near the
153
- // bottom. Requires `columnMaxHeight`, since an unbounded column never scrolls.
169
+ // bottom. Needs a column that scrolls: `columnMaxHeight` or a board with a height.
154
170
  // KanbanBoard tracks its own in-flight state per column (shown as a spinner at
155
- // the bottom of the list) and retries automatically if a page doesn't fill the
156
- // column's visible height, so pagination doesn't dead-end on a short first batch.
171
+ // the bottom of the list) and asks again while a column's cards leave empty space
172
+ // below them, so pagination doesn't dead-end on a short first batch.
157
173
  const columnsPaged: KanbanColumnDef[] = [
158
174
  { id: 'done', title: 'Done', itemIds: doneTaskIds, hasMore: doneHasMore },
159
175
  ];
@@ -171,10 +187,12 @@ const columnsPaged: KanbanColumnDef[] = [
171
187
  />
172
188
  ```
173
189
 
174
- **Keyboard support:** cards are focusable and can be picked up and moved with the keyboard — Space or Enter to pick up, Space or Enter again to drop, Escape to cancel (reverts to the original position). Arrow keys move the card: Up/Down reorders within its current column; Left/Right moves it into the adjacent column, landing on the nearest card there (or the column itself, if it's empty). This uses a board-aware coordinate getter rather than `dnd-kit`'s generic `sortableKeyboardCoordinates`, which — across multiple per-column `SortableContext`s — can't reliably tell "next card in this column" from "some other column's droppable" and ends up mixing up Up/Down and Left/Right. With `reorderableColumns`, a column's header is focusable the same way — Space/Enter to pick it up, Left/Right to move it, Up/Down does nothing (columns are a single row).
190
+ **Keyboard support:** cards are focusable, and the arrow keys move focus between them while nothing is picked up: Up/Down within a column, Left/Right to the nearest card in the next column. Space or Enter picks a card up, Space or Enter again drops it, and Escape cancels (reverts to the original position). While a card is picked up, the same arrow keys move the card instead: Up/Down reorders it within its column; Left/Right moves it into the adjacent column, landing on the nearest card there (or the column itself, if it's empty). This uses a board-aware coordinate getter rather than `dnd-kit`'s generic `sortableKeyboardCoordinates`, which, across multiple per-column `SortableContext`s, can't reliably tell "next card in this column" from "some other column's droppable" and ends up mixing up Up/Down and Left/Right. With `reorderableColumns`, each column's title is focusable too and joins the arrow-key navigation (Left/Right goes title to title, Up/Down between the title and the column's cards): Space/Enter picks the column up and Left/Right moves it. Keys pressed in a control inside a card or header, such as a `renderColumnActions` button or an input in a card, go to that control and never start a drag.
175
191
 
176
192
  **Column reordering (`reorderableColumns`):** a column's header becomes its drag handle for moving the whole column — dragging the cards area still only moves cards. This shares the same `DndContext` as card dragging (mouse, touch, and keyboard all work the same way), and commits through the same `onChange` — a column move is just a reordering of the `columns` array, same as any other change.
177
193
 
194
+ **Scrolling while dragging:** holding a card near the edge of a column's card list scrolls that list. The page scrolls only toward a part of the board that is out of view, and stops once the board's edge is on screen, since nothing outside the board takes a drop.
195
+
178
196
  **Screen-reader announcements:** every pick-up, hover, drop, and cancel is announced (e.g. "Picked up card Design mockup.", "Card Design mockup is over the In Progress column."). By default cards are announced by their id — pass `getItemLabel` to announce something meaningful instead (a title, a short description).
179
197
 
180
198
  **KanbanBoardProps:**
@@ -185,15 +203,15 @@ const columnsPaged: KanbanColumnDef[] = [
185
203
  | `items` | `Record<string, T>` | Flat map of all items keyed by id |
186
204
  | `renderItem` | `(item: T, id: string) => ReactNode` | Render function for each item |
187
205
  | `onChange` | `(columns: KanbanColumnDef[]) => void` | Called after a drop that actually changes column or order — replace state with this value |
188
- | `dragHandle` | `boolean` | Restrict drag initiation to elements marked `data-drag-handle` within a card, instead of the whole card. Use when `renderItem` includes interactive content |
189
- | `getItemLabel` | `(item: T, id: string) => string` | Accessible label for a card, used in screen-reader drag announcements. Defaults to the card's id |
206
+ | `dragHandle` | `boolean` | Drag a card only by the element marked `data-drag-handle` inside it (mouse, touch and keyboard), instead of by the whole card. The board makes the handle focusable and names it with `getItemLabel` unless it has an `aria-label`, and the card stops being a button. Use when `renderItem` includes controls: without it the whole card is one button, its text can't be selected, and a screen reader reads controls inside it as plain text |
207
+ | `getItemLabel` | `(item: T, id: string) => string` | Accessible label for a card, used in screen-reader drag announcements and as the name of a `dragHandle` handle without one. Defaults to the card's id |
190
208
  | `getItemDisabled` | `(item: T, id: string) => boolean` | Returns whether a specific card can't be picked up. Defaults to every card being draggable; independent of a column's own `disabled`. A pinned card stays a valid drop target — other cards can still be reordered before/after it, unlike a card inside a fully frozen (`disabled`) column |
191
209
  | `renderEmptyColumn` | `(column: KanbanColumnDef) => ReactNode` | Custom content shown inside a column when it has no cards, instead of leaving it blank |
192
210
  | `renderColumnActions` | `(column: KanbanColumnDef) => ReactNode` | Custom actions rendered in a column's header, next to its title (e.g. an "add card" button, a column menu) |
193
211
  | `reorderableColumns` | `boolean` | Allow whole columns to be reordered by dragging their header, alongside card dragging (default `false`) |
194
- | `collapseEmptyColumns` | `boolean` | Shrink a column to a narrow strip once it has no cards (default `true`). Set `false` to keep every column the same width regardless of card count. The collapsed width is set via `--kanban-empty-column-width` (falls back to `140px`) — override it theme-wide via `ThemeProvider`, or per board via `style` |
195
- | `columnMaxHeight` | `string` | Bounds each column's height (any CSS length, e.g. `'70vh'`) and makes its card list scroll independently once content overflows, instead of growing the whole page. Unset by default (unbounded height). Required for `onLoadMore` to ever trigger |
196
- | `onLoadMore` | `(columnId: string) => void \| Promise<void>` | Called when a column with `hasMore` is scrolled within `loadMoreThreshold` of the bottom — fetch and append the next page for that column. Tracked per column, so overlapping calls for the same column are never made concurrently; retried automatically if a loaded batch doesn't fill the column's own visible height |
212
+ | `collapseEmptyColumns` | `boolean` | Shrink a column to a narrow strip while it has no cards (default `false`). Widths hold still during a drag: a column the drag empties or fills changes width after the drop. The collapsed width is set via `--kanban-empty-column-width` (falls back to `140px`); override it theme-wide via `ThemeProvider`, or per board via `style` |
213
+ | `columnMaxHeight` | `string` | Caps each column's card list at a length (any CSS length, e.g. `'70vh'`), so a long column scrolls on its own instead of growing the page. Unset by default. To fill the space your layout gives the board instead, give the board a height (`style={{ height: '100%' }}` in a parent with one, or `flex: 1; min-height: 0` in a flex column): its columns then scroll the same way |
214
+ | `onLoadMore` | `(columnId: string) => void \| Promise<void>` | Called when a column with `hasMore` is scrolled within `loadMoreThreshold` of the bottom: fetch and append the next page for that column. Tracked per column, so overlapping calls for the same column are never made concurrently. Also called while the column's cards leave empty space below them. Paging all the way needs a column that scrolls (`columnMaxHeight` or a board with a height); on a board that grows with its cards, a short column only fills up to the tallest one |
197
215
  | `loadMoreThreshold` | `number` | Distance in px from the bottom of a column's scroll area that triggers `onLoadMore` (default `100`, matching `VirtualList`) |
198
216
  | `id` / `role` / `aria-*` / `data-*` | | Passed through to the root element |
199
217
 
package/docs/Textarea.md CHANGED
@@ -36,7 +36,7 @@ const descValidator = new FormValidator('', [Validators.required(), Validators.m
36
36
  | `value` | `string` | Controlled value |
37
37
  | `onChange` | `(value: string) => void` | Change handler |
38
38
  | `rows` | `number` | Initial visible row count |
39
- | `autoResize` | `boolean` | Grows with content |
39
+ | `autoResize` | `boolean` | Grows with content, in place of the resize grip. To drop the grip without it: `styles={{ container: { resize: 'none' } }}` |
40
40
  | `formValidator` | `FormValidator` | Connects to validation |
41
41
  | `placeholder` | `string` | |
42
42
  | `readOnly` | `boolean` | |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/ui",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "A React UI component library with theming, CSS Modules, and TypeScript support",
5
5
  "keywords": [
6
6
  "agent",