@ahrowe/ui 0.48.0 → 0.50.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 (83) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +3 -0
  3. package/dist/cjs-types/common/actionIcon/actionIcon.types.d.ts +1 -1
  4. package/dist/cjs-types/common/button/button.types.d.ts +6 -1
  5. package/dist/cjs-types/common/chatComposer/chatComposer.d.ts +4 -0
  6. package/dist/cjs-types/common/chatComposer/chatComposer.types.d.ts +40 -0
  7. package/dist/cjs-types/common/chatComposer/index.d.ts +2 -0
  8. package/dist/cjs-types/common/chatMessage/chatMessage.d.ts +4 -0
  9. package/dist/cjs-types/common/chatMessage/chatMessage.types.d.ts +60 -0
  10. package/dist/cjs-types/common/chatMessage/index.d.ts +2 -0
  11. package/dist/cjs-types/common/chatMessageList/chatMessageList.d.ts +4 -0
  12. package/dist/cjs-types/common/chatMessageList/chatMessageList.types.d.ts +27 -0
  13. package/dist/cjs-types/common/chatMessageList/index.d.ts +2 -0
  14. package/dist/cjs-types/common/configProvider/configProvider.types.d.ts +6 -0
  15. package/dist/cjs-types/common/hooks/useAutoGrow.d.ts +12 -0
  16. package/dist/cjs-types/common/hooks/useHotkeys.d.ts +11 -0
  17. package/dist/cjs-types/common/tabHeader/tabHeader.types.d.ts +5 -3
  18. package/dist/cjs-types/index.d.ts +6 -0
  19. package/dist/esm/common/button/button.mjs +1 -1
  20. package/dist/esm/common/button/button.mjs.map +1 -1
  21. package/dist/esm/common/button/button.module.mjs +1 -1
  22. package/dist/esm/common/button/button.module.mjs.map +1 -1
  23. package/dist/esm/common/button/button.types.mjs.map +1 -1
  24. package/dist/esm/common/chatComposer/chatComposer.mjs +2 -0
  25. package/dist/esm/common/chatComposer/chatComposer.mjs.map +1 -0
  26. package/dist/esm/common/chatComposer/chatComposer.module.mjs +2 -0
  27. package/dist/esm/common/chatComposer/chatComposer.module.mjs.map +1 -0
  28. package/dist/esm/common/chatMessage/chatMessage.mjs +2 -0
  29. package/dist/esm/common/chatMessage/chatMessage.mjs.map +1 -0
  30. package/dist/esm/common/chatMessage/chatMessage.module.mjs +2 -0
  31. package/dist/esm/common/chatMessage/chatMessage.module.mjs.map +1 -0
  32. package/dist/esm/common/chatMessage/chatMessage.types.mjs +2 -0
  33. package/dist/esm/common/chatMessage/chatMessage.types.mjs.map +1 -0
  34. package/dist/esm/common/chatMessageList/chatMessageList.mjs +2 -0
  35. package/dist/esm/common/chatMessageList/chatMessageList.mjs.map +1 -0
  36. package/dist/esm/common/chatMessageList/chatMessageList.module.mjs +2 -0
  37. package/dist/esm/common/chatMessageList/chatMessageList.module.mjs.map +1 -0
  38. package/dist/esm/common/commandPalette/commandPalette.mjs +1 -1
  39. package/dist/esm/common/commandPalette/commandPalette.mjs.map +1 -1
  40. package/dist/esm/common/dropdown/dropdown.mjs +1 -1
  41. package/dist/esm/common/hooks/useAutoGrow.mjs +2 -0
  42. package/dist/esm/common/hooks/useAutoGrow.mjs.map +1 -0
  43. package/dist/esm/common/hooks/useHotkeys.mjs +1 -1
  44. package/dist/esm/common/hooks/useHotkeys.mjs.map +1 -1
  45. package/dist/esm/common/infiniteBlock/useInfiniteScroll.mjs +1 -1
  46. package/dist/esm/common/infiniteBlock/useInfiniteScroll.mjs.map +1 -1
  47. package/dist/esm/common/input/input.mjs +1 -1
  48. package/dist/esm/common/input/input.mjs.map +1 -1
  49. package/dist/esm/common/menu/menu.mjs +1 -1
  50. package/dist/esm/common/menu/menu.mjs.map +1 -1
  51. package/dist/esm/common/tabHeader/tabHeader.mjs +1 -1
  52. package/dist/esm/common/tabHeader/tabHeader.mjs.map +1 -1
  53. package/dist/esm/index.mjs +1 -1
  54. package/dist/index.cjs +3 -3
  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 +1 -1
  58. package/dist/types/common/button/button.types.d.ts +6 -1
  59. package/dist/types/common/chatComposer/chatComposer.d.ts +4 -0
  60. package/dist/types/common/chatComposer/chatComposer.types.d.ts +40 -0
  61. package/dist/types/common/chatComposer/index.d.ts +2 -0
  62. package/dist/types/common/chatMessage/chatMessage.d.ts +4 -0
  63. package/dist/types/common/chatMessage/chatMessage.types.d.ts +60 -0
  64. package/dist/types/common/chatMessage/index.d.ts +2 -0
  65. package/dist/types/common/chatMessageList/chatMessageList.d.ts +4 -0
  66. package/dist/types/common/chatMessageList/chatMessageList.types.d.ts +27 -0
  67. package/dist/types/common/chatMessageList/index.d.ts +2 -0
  68. package/dist/types/common/configProvider/configProvider.types.d.ts +6 -0
  69. package/dist/types/common/hooks/useAutoGrow.d.ts +12 -0
  70. package/dist/types/common/hooks/useHotkeys.d.ts +11 -0
  71. package/dist/types/common/tabHeader/tabHeader.types.d.ts +5 -3
  72. package/dist/types/index.d.ts +6 -0
  73. package/docs/ActionIcon.md +2 -2
  74. package/docs/Button.md +18 -2
  75. package/docs/CLAUDE.md +6 -3
  76. package/docs/ChatComposer.md +100 -0
  77. package/docs/ChatMessage.md +88 -0
  78. package/docs/ChatMessageList.md +113 -0
  79. package/docs/ConfigProvider.md +1 -0
  80. package/docs/Hooks.md +23 -4
  81. package/docs/Menu.md +1 -1
  82. package/docs/TabHeader.md +19 -8
  83. package/package.json +1 -1
@@ -0,0 +1,113 @@
1
+ # ChatMessageList
2
+
3
+ **When to use:** The scrolling conversation of a chat: a messenger, a support widget, an AI assistant. It opens at the newest message, follows new ones while the user is at the bottom, and loads older ones at the top without moving the view. Fill it with [ChatMessage](ChatMessage.md)s and put a [ChatComposer](ChatComposer.md) under it. For a long list that is not a conversation, use [VirtualList](VirtualList.md).
4
+
5
+ **Keywords:** messenger, whatsapp, support chat, chatbot, assistant, LLM, history, thread, stick to bottom, autoscroll, infinite scroll, load older, jump to latest, unread
6
+
7
+ **Import:** `import { ChatMessageList } from '@ahrowe/ui'`
8
+ **Types:** `import type { ChatMessageListProps, ChatMessageListHandle, ChatMessageListLabels } from '@ahrowe/ui'`
9
+
10
+ ## A whole chat
11
+
12
+ ```tsx
13
+ import { useRef, useState } from 'react';
14
+ import { Avatar, ChatComposer, ChatMessage, ChatMessageList, ChatMessageStatus, Divider } from '@ahrowe/ui';
15
+ import type { ChatMessageListHandle } from '@ahrowe/ui';
16
+
17
+ function Chat({ me, initial }: ChatProps) {
18
+ const [messages, setMessages] = useState<Message[]>(initial);
19
+ const [hasOlder, setHasOlder] = useState(true);
20
+ const listRef = useRef<ChatMessageListHandle>(null);
21
+
22
+ const loadOlder = async () => {
23
+ const page = await api.messagesBefore(messages[0].id);
24
+ setMessages((current) => [...page.messages, ...current]);
25
+ setHasOlder(page.hasMore);
26
+ };
27
+
28
+ const send = (text: string) => {
29
+ setMessages((current) => [...current, { id: crypto.randomUUID(), authorId: me.id, text, status: ChatMessageStatus.Sending }]);
30
+ // Sending from further up the history takes the user down to their message.
31
+ listRef.current?.scrollToBottom();
32
+ };
33
+
34
+ return (
35
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}>
36
+ <ChatMessageList ref={listRef} style={{ flex: 1 }} onLoadMore={hasOlder ? loadOlder : undefined}>
37
+ <Divider text='Today' />
38
+ {messages.map((message, index) => (
39
+ <ChatMessage
40
+ key={message.id}
41
+ isOwn={message.authorId === me.id}
42
+ isContinued={messages[index - 1]?.authorId === message.authorId}
43
+ avatar={message.authorId === me.id ? undefined : <Avatar name={message.authorName} />}
44
+ author={message.authorId === me.id ? undefined : message.authorName}
45
+ status={message.status}
46
+ >
47
+ {message.text}
48
+ </ChatMessage>
49
+ ))}
50
+ </ChatMessageList>
51
+ <ChatComposer onSend={send} />
52
+ </div>
53
+ );
54
+ }
55
+ ```
56
+
57
+ An AI chat is the same with a `Plain` answer that grows as it streams, and the composer's stop
58
+ button while it does:
59
+
60
+ ```tsx
61
+ <ChatMessageList ref={listRef} style={{ flex: 1 }}>
62
+ {turns.map((turn) =>
63
+ turn.role === 'user' ? (
64
+ <ChatMessage key={turn.id} isOwn>{turn.text}</ChatMessage>
65
+ ) : (
66
+ <ChatMessage key={turn.id} styleType={ChatMessageStyleType.Plain} actions={<CopyButton value={turn.text} />}>
67
+ <Markdown>{turn.text}</Markdown>
68
+ </ChatMessage>
69
+ ),
70
+ )}
71
+ </ChatMessageList>
72
+ <ChatComposer onSend={ask} onStop={isStreaming ? stop : undefined} />
73
+ ```
74
+
75
+ ## Scrolling
76
+
77
+ - **Following.** While the user is within 32px of the bottom, anything that makes the content
78
+ taller keeps the newest message in view: a new message, an answer streaming in, an image
79
+ loading, the list getting shorter because the composer grew or a phone keyboard opened.
80
+ - **Reading back.** As soon as the user scrolls up, however little, nothing moves under them: new
81
+ messages arrive below, and a round button appears in the corner that jumps back down and starts
82
+ following again. It turns `Primary` once a new message has arrived below. Scrolling back to the
83
+ bottom by hand does the same.
84
+ - **After sending**, call `scrollToBottom()` on the handle, or a user who sent from further up
85
+ never sees their own message arrive.
86
+ - **Older messages.** With `onLoadMore`, reaching the top calls it and shows a loader there.
87
+ Prepend what it loads; the message that was at the top stays exactly where it was, at the very
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.
92
+
93
+ **Sizing.** The list scrolls itself and fills the height it is given: `flex: 1` in a column, as
94
+ above, or a `height`. A conversation shorter than the list sits at the bottom, by the composer. To
95
+ start at the top instead, as an AI chat often does: `styles={{ content: { marginTop: 0 } }}`.
96
+
97
+ **Key props:**
98
+
99
+ | Prop | Type | Description |
100
+ |------|------|-------------|
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 |
103
+ | `ref` | `Ref<ChatMessageListHandle>` | `scrollToBottom()`: back to the newest message, smoothly unless the user prefers reduced motion, and following again |
104
+
105
+ **Accessibility:** the scrolling element is a `role="log"`, which screen readers announce
106
+ politely as messages are added, named "Messages" (`labels.messages`). It is a tab stop of its own,
107
+ so the history can be scrolled from the keyboard, and `aria-busy` while older messages load.
108
+
109
+ **Global defaults:** adopts `ConfigProvider`. See [ConfigProvider.md](ConfigProvider.md).
110
+
111
+ **Slots:** `viewport` (the scrolling log) `content` (the column of messages inside it) `scrollButton`
112
+
113
+ **Labels:** `messages` `scrollToBottom`: the text this component renders of its own. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `ChatMessageListLabels`.
@@ -97,6 +97,7 @@ Each entry is a `Partial<...Props>`, so any of that component's props can be def
97
97
  - **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker` · `OptionPicker` · `OtpInput`
98
98
  - **Display:** `Button` · `CopyButton` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Loader` · `LoadingOverlay` · `Accordion` · `Divider` · `Timer` · `WeatherIcon` · `Stepper`
99
99
  - **Overlays:** `ConfirmModal` · `Modal` · `CommandPalette` · `Popover`
100
+ - **Chat:** `ChatMessage` · `ChatMessageList` · `ChatComposer`
100
101
  - **Editors:** `RoomDrawer`, `RoomViewer`
101
102
  - **Scheduling:** `Calendar`, for `labels`, `locale`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays`, `scrollToHour`, `workingHours` and `showWeekNumbers` only (see below)
102
103
 
package/docs/Hooks.md CHANGED
@@ -59,6 +59,7 @@ Two rules worth knowing:
59
59
  | `target` | `RefObject<HTMLElement \| null>` | window | Scope the shortcuts to one element |
60
60
  | `enableInFields` | `boolean` | `false` | Let a combo fire while a text field or `contenteditable` has focus. Only combos the field would otherwise consume are held back: a single character, or an editing key (`arrowup`, `home`, `backspace`, `enter`, `tab`, …). Anything carrying Ctrl, Cmd or Alt fires regardless, and so do `f2`, `escape` and the other keys a field does not use |
61
61
  | `preventDefault` | `boolean` | `true` | Call `preventDefault()` on a match, so `mod+s` does not also open the browser's save dialog |
62
+ | `repeat` | `boolean` | `false` | Fire again for each repeat while the combo is held. Off, holding `mod+s` saves once; turn it on for keys that step through something |
62
63
 
63
64
  Shortcuts keep firing while a `Modal` or `Drawer` is open, which is usually what a command palette
64
65
  wants. Pass `enabled` for the ones that should not:
@@ -67,7 +68,14 @@ wants. Pass `enabled` for the ones that should not:
67
68
  useHotkeys({ n: createRecord }, { enabled: !isModalOpen });
68
69
  ```
69
70
 
70
- A held key repeats, as it does natively. Check `event.repeat` in the handler to ignore repeats.
71
+ A held combo fires once, where the browser's key repeat would save over and over or flicker a
72
+ toggle. The skipped repeats are still `preventDefault()`-ed. Where a repeat is the point, as with
73
+ arrows or zoom, ask for it, in a call of its own if the page also has actions:
74
+
75
+ ```tsx
76
+ useHotkeys({ 'mod+s': save, '?': toggleHelp });
77
+ useHotkeys({ j: selectNext, k: selectPrevious }, { repeat: true });
78
+ ```
71
79
 
72
80
  ### formatHotkey
73
81
 
@@ -79,9 +87,10 @@ formatHotkey('mod+shift+k'); // '⇧⌘K' on an Apple platform, 'Ctrl+Shift+K' e
79
87
  formatHotkey('f2'); // 'F2'
80
88
  ```
81
89
 
82
- This is what `Menu` uses to draw the shortcut on an entry, from the same string `menuHotkeys`
83
- turns into a binding, see [Menu.md](Menu.md). A combo it cannot parse is handed back unchanged. The platform is read at call
84
- time, so a server-rendered page renders the non-Apple spelling.
90
+ This is what `Menu`, `CommandPalette` and `Button` use to draw a shortcut, from the same string
91
+ `useHotkeys` binds, see [Menu.md](Menu.md) and [Button.md](Button.md). A combo it cannot parse is
92
+ handed back unchanged. The platform is read at call time, so a server-rendered page renders the
93
+ non-Apple spelling.
85
94
 
86
95
  ## useMediaQuery
87
96
 
@@ -266,3 +275,13 @@ Hooks documented with the thing they belong to:
266
275
  - `useComponentDefaults`, `useLabels` — see [ConfigProvider.md](ConfigProvider.md)
267
276
  - `useFormValidator` — see [FormValidator.md](FormValidator.md)
268
277
  - `useFormValidatorGroup` — see [FormValidatorGroup.md](FormValidatorGroup.md)
278
+
279
+ ## Migration from repeating hotkeys
280
+
281
+ Before 0.49, a held `useHotkeys` combo fired its handler for every repeat the browser sent. It now
282
+ fires once. Nothing fails to compile, so look for shortcuts that are meant to be held:
283
+
284
+ | Before | After |
285
+ |--------|-------|
286
+ | `useHotkeys({ arrowdown: next })`, relying on the repeat | `useHotkeys({ arrowdown: next }, { repeat: true })` |
287
+ | `if (event.repeat) return;` at the top of a handler | nothing: drop the check |
package/docs/Menu.md CHANGED
@@ -34,7 +34,7 @@ const items: MenuEntry[] = [
34
34
 
35
35
  `keywords` are extra search terms for [CommandPalette](CommandPalette.md), which takes the same entries. The menu ignores them.
36
36
 
37
- **Shortcuts are drawn, not bound.** A `shortcut` is a combo in [useHotkeys](Hooks.md) spelling, and the menu writes it on the trailing edge for the platform the reader is on: `mod+s` shows as `⌘S` on an Apple one and `Ctrl+S` everywhere else. So there is one spelling per shortcut and a hand-written `'⌘S'` can no longer be wrong on Windows.
37
+ **Shortcuts are drawn, not bound.** A `shortcut` is a combo in [useHotkeys](Hooks.md) spelling, and the menu writes it on the trailing edge for the platform the reader is on: `mod+s` shows as `⌘S` on an Apple one and `Ctrl+S` everywhere else. So there is one spelling per shortcut and a hand-written `'⌘S'` can no longer be wrong on Windows. Screen readers get it through `aria-keyshortcuts`, not as part of the item's name.
38
38
 
39
39
  Listening for it is yours, because only your app knows what else is listening: if it already binds `⌘S` and the menu bound it too, both would fire and the action would run twice. `menuHotkeys` turns the same entries into a map for `useHotkeys`, so the one spelling covers the hint and the binding:
40
40
 
package/docs/TabHeader.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # TabHeader
2
2
 
3
- **When to use:** A horizontal row of tabs to switch between sibling views or sections — settings panels, dashboards, detail pages. Each tab carries an icon plus an optional title and subtitle.
3
+ **When to use:** A horizontal row of tabs to switch between sibling views or sections — settings panels, dashboards, detail pages. Each tab carries an icon plus an optional title, subtitle and end content such as a badge.
4
4
 
5
- **Keywords:** tabs navigation, tab bar
5
+ **Keywords:** tabs navigation, tab bar, pill, counter
6
6
 
7
7
  **Import:** `import { TabHeader } from '@ahrowe/ui'`
8
8
 
9
- **Mobile behaviour:** On narrow screens (≤ 640px) only the active tab keeps its title and subtitle; every other tab collapses to just its icon.
9
+ **Mobile behaviour:** On narrow screens (≤ 640px) only the active tab keeps its title and subtitle; every other tab collapses to its icon and `endContent`.
10
10
 
11
11
  ```tsx
12
12
  import { useState } from 'react';
13
- import { TabHeader } from '@ahrowe/ui';
13
+ import { TabHeader, Badge, ThemeColor } from '@ahrowe/ui';
14
14
  import type { TabHeaderTab } from '@ahrowe/ui';
15
- import { faUser, faBell, faGear } from '@fortawesome/free-solid-svg-icons';
15
+ import { faUser, faBell, faGear, faInbox, faAt } from '@fortawesome/free-solid-svg-icons';
16
16
 
17
17
  const TABS: TabHeaderTab[] = [
18
18
  { id: 'profile', icon: faUser, title: 'Profile', subtitle: 'Your details' },
@@ -32,6 +32,16 @@ function Example() {
32
32
  onChange={setTab}
33
33
  />
34
34
 
35
+ // A count next to the title, still shown when a narrow screen collapses the tab
36
+ <TabHeader
37
+ tabs={[
38
+ { id: 'inbox', icon: faInbox, title: 'Inbox', endContent: <Badge count={12} /> },
39
+ { id: 'mentions', icon: faAt, title: 'Mentions', endContent: <Badge count={3} color={ThemeColor.Error} /> },
40
+ ]}
41
+ currentTab={tab}
42
+ onChange={setTab}
43
+ />
44
+
35
45
  // Disabled tab — dimmed and not selectable
36
46
  <TabHeader
37
47
  tabs={[
@@ -57,8 +67,9 @@ function Example() {
57
67
  |-------|------|-------------|
58
68
  | `id` | `string` | Unique identifier, matched against `currentTab` |
59
69
  | `icon` | `IconDefinition \| ReactElement` | FontAwesome icon or any React element (always visible, incl. mobile) |
60
- | `title` | `string` | Optional title next to the icon |
61
- | `subtitle` | `string` | Optional subtitle beneath the title |
70
+ | `title` | `ReactNode` | Optional title next to the icon |
71
+ | `subtitle` | `ReactNode` | Optional subtitle beneath the title |
72
+ | `endContent` | `ReactNode` | After the labels. Stays visible when a narrow screen collapses the tab, so keep it small: a `Badge`, a dot |
62
73
  | `disabled` | `boolean` | Dim the tab and prevent selection |
63
74
 
64
- **Slots:** `root` `tab` `icon` `labels` `title` `subtitle`
75
+ **Slots:** `root` `tab` `icon` `labels` `title` `subtitle` `endContent`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/ui",
3
- "version": "0.48.0",
3
+ "version": "0.50.0",
4
4
  "description": "A React UI component library with theming, CSS Modules, and TypeScript support",
5
5
  "keywords": [
6
6
  "agent",