blixify-ui-web-mcp 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/chat/chatAnchor/README.md +24 -22
- package/lib/components/chat/chatBase/README.md +47 -29
- package/lib/components/chat/chatBase/chatBase.stories.tsx +37 -10
- package/lib/components/chat/chatDashboard/README.md +5 -1
- package/lib/components/chat/chatDashboard/chatDashboard.stories.tsx +102 -38
- package/lib/components/display/newList/README.md +25 -13
- package/lib/components/input/markdown/README.md +80 -0
- package/lib/components/input/markdown/markdown.stories.tsx +1 -1
- package/lib/components/input/markdown/markdownImageResize.stories.tsx +60 -0
- package/package.json +1 -1
|
@@ -88,25 +88,27 @@ return (
|
|
|
88
88
|
|
|
89
89
|
##### Optional props — inherited from ChatBase
|
|
90
90
|
|
|
91
|
-
| Name
|
|
92
|
-
|
|
|
93
|
-
| `headerElement`
|
|
94
|
-
| `bodyElement`
|
|
95
|
-
| `footerElement`
|
|
96
|
-
| `additionalAction`
|
|
97
|
-
| `hideAdditionalAction`
|
|
98
|
-
| `disableMessageInput`
|
|
99
|
-
| `isLoadingReply`
|
|
100
|
-
| `className`
|
|
101
|
-
| `sizeClassName`
|
|
102
|
-
| `disableMobileResize`
|
|
103
|
-
| `paddingTop`
|
|
104
|
-
| `searchMessage`
|
|
105
|
-
| `onSendMessage`
|
|
106
|
-
| `onChangeMessage`
|
|
107
|
-
| `onSearch`
|
|
108
|
-
| `onChangeSearch`
|
|
109
|
-
| `onClose`
|
|
110
|
-
| `onHiddenChatText`
|
|
111
|
-
| `fileUpload`
|
|
112
|
-
| `lib`
|
|
91
|
+
| Name | Type | Description |
|
|
92
|
+
| ----------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
93
|
+
| `headerElement` | `ReactElement` | Custom header rendered at the top of the chat panel |
|
|
94
|
+
| `bodyElement` | `ReactElement` | Replaces the default message list with a custom element |
|
|
95
|
+
| `footerElement` | `ReactElement` | Rendered below the message list, above the input bar |
|
|
96
|
+
| `additionalAction` | `ChatBaseAction[]` | Extra items added to the actions dropdown menu |
|
|
97
|
+
| `hideAdditionalAction` | `boolean` | Hides the actions dropdown entirely |
|
|
98
|
+
| `disableMessageInput` | `boolean` | Disables the text input |
|
|
99
|
+
| `isLoadingReply` | `boolean` | Shows a typing indicator and disables input while waiting for a reply |
|
|
100
|
+
| `className` | `string` | Extra classes applied to the chat panel container |
|
|
101
|
+
| `sizeClassName` | `string` | Overrides the default panel size (`h-[75vh] w-96` on desktop) |
|
|
102
|
+
| `disableMobileResize` | `boolean` | Prevents the panel from going full screen on mobile |
|
|
103
|
+
| `paddingTop` | `number` | Top padding (px) applied to the chat panel — useful when a sticky bar overlaps |
|
|
104
|
+
| `searchMessage` | `string` | Current search query value |
|
|
105
|
+
| `onSendMessage` | `(conversation: Conversation) => void` | Called with the updated conversation when a message is sent |
|
|
106
|
+
| `onChangeMessage` | `(event: any) => void` | Called on every keystroke in the message input |
|
|
107
|
+
| `onSearch` | `(searchMessage: string) => void` | Called when the search is submitted |
|
|
108
|
+
| `onChangeSearch` | `(event: any) => void` | Called on every keystroke in the search input |
|
|
109
|
+
| `onClose` | `() => void` | Called when the mobile close button is tapped |
|
|
110
|
+
| `onHiddenChatText` | `boolean` | Hides the text input and file clip icon entirely |
|
|
111
|
+
| `fileUpload` | `FileUpload` | Configuration object for file upload support |
|
|
112
|
+
| `lib` | `{ axios?: any }` | Additional libraries (e.g. axios for file uploads) |
|
|
113
|
+
| `darkMode` | `boolean` | Dark surfaces for the chat panel |
|
|
114
|
+
| `disableComposerResize` | `boolean` | Removes the drag handle on the message box's top edge — see ChatBase "Message box resize" |
|
|
@@ -153,38 +153,56 @@ const toolCallMessage: Message = {
|
|
|
153
153
|
|
|
154
154
|
##### Optional props
|
|
155
155
|
|
|
156
|
-
| Name
|
|
157
|
-
|
|
|
158
|
-
| `fileUpload`
|
|
159
|
-
| `searchMessage`
|
|
160
|
-
| `headerElement`
|
|
161
|
-
| `bodyElement`
|
|
162
|
-
| `footerElement`
|
|
163
|
-
| `additionalAction`
|
|
164
|
-
| `hideAdditionalAction`
|
|
165
|
-
| `disableMessageInput`
|
|
166
|
-
| `isLoadingReply`
|
|
167
|
-
| `onHiddenChatText`
|
|
168
|
-
| `disableMobileResize`
|
|
169
|
-
| `sizeClassName`
|
|
170
|
-
| `className`
|
|
171
|
-
| `paddingTop`
|
|
172
|
-
| `inputActions`
|
|
173
|
-
| `onSendMessage`
|
|
174
|
-
| `onChangeMessage`
|
|
175
|
-
| `onSearch`
|
|
176
|
-
| `onChangeSearch`
|
|
177
|
-
| `onClose`
|
|
178
|
-
| `lib`
|
|
156
|
+
| Name | Type | Default | Description |
|
|
157
|
+
| ----------------------- | -------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `fileUpload` | `FileUpload` | — | Enables file upload — see FileUpload below |
|
|
159
|
+
| `searchMessage` | `string` | — | Current search input value. Pass the filtered conversation to `conversation` to show results. |
|
|
160
|
+
| `headerElement` | `ReactElement` | — | Replaces the default header, which shows the other member's avatar and name ("Chat" when there is none) |
|
|
161
|
+
| `bodyElement` | `ReactElement` | — | Replaces the default message list. Useful for loading states. |
|
|
162
|
+
| `footerElement` | `ReactElement` | — | Rendered above the message input. Useful for limit warnings or disclaimers. |
|
|
163
|
+
| `additionalAction` | `ChatBaseAction[]` | — | Extra items added to the ⋮ actions dropdown — see ChatBaseAction below |
|
|
164
|
+
| `hideAdditionalAction` | `boolean` | `false` | Hides the ⋮ actions dropdown entirely |
|
|
165
|
+
| `disableMessageInput` | `boolean` | `false` | Disables the message text input |
|
|
166
|
+
| `isLoadingReply` | `boolean` | `false` | Shows an animated typing indicator above the input and disables the input |
|
|
167
|
+
| `onHiddenChatText` | `boolean` | `false` | Hides the text input and paperclip icon. Useful when the input is managed externally. |
|
|
168
|
+
| `disableMobileResize` | `boolean` | `false` | Prevents the component from expanding to full screen on mobile |
|
|
169
|
+
| `sizeClassName` | `string` | `h-full w-full sm:h-[75vh] sm:w-96` | Tailwind classes controlling the component dimensions |
|
|
170
|
+
| `className` | `string` | — | Additional Tailwind classes applied to the outer container |
|
|
171
|
+
| `paddingTop` | `number` | `0` | Top padding in pixels. Used to offset the actions menu in mobile app layouts. |
|
|
172
|
+
| `inputActions` | `ReactElement[]` | — | Extra icon buttons rendered in the input row between the paperclip and the send button |
|
|
173
|
+
| `onSendMessage` | `(conversation: Conversation) => void` | — | Called with the updated conversation (including the new message) when send is triggered |
|
|
174
|
+
| `onChangeMessage` | `(event: any) => void` | — | Called on every keystroke in the message input |
|
|
175
|
+
| `onSearch` | `(searchMessage: string) => void` | — | Called when the search form is submitted |
|
|
176
|
+
| `onChangeSearch` | `(event: any) => void` | — | Called on every keystroke in the search input |
|
|
177
|
+
| `onClose` | `() => void` | — | Called when the mobile close (✕) button is tapped |
|
|
178
|
+
| `lib` | `{ axios?: any }` | — | Pass axios for file upload operations |
|
|
179
|
+
| `darkMode` | `boolean` | `false` | Dark surfaces for the header, message list, bubbles and message box |
|
|
180
|
+
| `disableComposerResize` | `boolean` | `false` | Removes the drag handle on the message box's top edge, so the box keeps its size |
|
|
181
|
+
|
|
182
|
+
##### Message box resize
|
|
183
|
+
|
|
184
|
+
A handle on the top edge of the message box (`data-testid="chat-composer-resize-handle"`, `role="separator"`) changes the box's height in whole lines. It is on by default; `disableComposerResize` removes it.
|
|
185
|
+
|
|
186
|
+
- Dragging up adds a line for every line-height moved; dragging down removes lines. Mouse, touch and pen use the same pointer events.
|
|
187
|
+
- ArrowUp / ArrowDown on the focused handle change it by one line. Double-click resets it.
|
|
188
|
+
- The height stays between `multiLine ?? 1` lines and the smaller of 12 lines and half the chat's height. The message list shrinks to make room and stays on the latest message when it was already there.
|
|
189
|
+
- The line count is stored in `localStorage` under `chat-composer-height`, read on mount, and ignored when storage is unavailable.
|
|
190
|
+
- Enter sends and Shift+Enter adds a line at any height. With `multiLine` set, or on a touch-first device (`pointer: coarse`, e.g. a phone), Enter adds a line and the send button sends.
|
|
191
|
+
|
|
192
|
+
The outer container carries `data-testid="chat-base"`.
|
|
193
|
+
|
|
194
|
+
##### renderChatAvatar
|
|
195
|
+
|
|
196
|
+
`renderChatAvatar(user, sizeClassName?, darkMode?)` is exported alongside `ChatBase`. It renders `user.imageUrl` as a round image, or the user's initials when there is no image. ChatDashboard uses it for the conversation list.
|
|
179
197
|
|
|
180
198
|
##### FileUpload
|
|
181
199
|
|
|
182
|
-
| Name | Type | Description
|
|
183
|
-
| ----------------- | ---------------------- |
|
|
184
|
-
| `allowFileUpload` | `boolean` | Shows the paperclip button to trigger file selection
|
|
185
|
-
| `hasFiles` | `boolean` | Tells the component files are selected — shows the send button and triggers `onSendMessage` on send
|
|
186
|
-
| `multiple` | `boolean` | Allows picking multiple files at once
|
|
187
|
-
| `acceptFileType` | `string` | Comma-separated accepted extensions, e.g. `".jpg,.png,.mp4,.pdf,.docx"`. Defaults to images only if omitted.
|
|
200
|
+
| Name | Type | Description |
|
|
201
|
+
| ----------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
202
|
+
| `allowFileUpload` | `boolean` | Shows the paperclip button to trigger file selection |
|
|
203
|
+
| `hasFiles` | `boolean` | Tells the component files are selected — shows the send button and triggers `onSendMessage` on send |
|
|
204
|
+
| `multiple` | `boolean` | Allows picking multiple files at once |
|
|
205
|
+
| `acceptFileType` | `string` | Comma-separated accepted extensions, e.g. `".jpg,.png,.mp4,.pdf,.docx"`. Defaults to images only if omitted. |
|
|
188
206
|
| `onChangeUpload` | `(event: any) => void` | Called when files are selected via the file input. Consumer extracts files from `event.target.files` and manages their own `File[]` state |
|
|
189
207
|
|
|
190
208
|
##### File rendering in message bubbles
|
|
@@ -3,7 +3,13 @@ import { MicrophoneIcon, CameraIcon } from "@heroicons/react/24/outline";
|
|
|
3
3
|
import { DocumentIcon, XMarkIcon } from "@heroicons/react/24/solid";
|
|
4
4
|
import dayjs from "../../dayjs";
|
|
5
5
|
import React, { useState } from "react";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ChatBase as ChatBaseImpl,
|
|
8
|
+
ChatBaseAction,
|
|
9
|
+
Conversation,
|
|
10
|
+
Message,
|
|
11
|
+
ToolCall,
|
|
12
|
+
} from ".";
|
|
7
13
|
import Logo from "../../../assets/logo.png";
|
|
8
14
|
import { StorySection } from "../../storyUtils";
|
|
9
15
|
import { TextButton } from "../../action/textButton";
|
|
@@ -51,7 +57,7 @@ const seedMessages: Message[] = [
|
|
|
51
57
|
];
|
|
52
58
|
|
|
53
59
|
const makeConversation = (
|
|
54
|
-
messages: Message[] = seedMessages
|
|
60
|
+
messages: Message[] = seedMessages,
|
|
55
61
|
): Conversation => ({
|
|
56
62
|
id: "1",
|
|
57
63
|
members: [currentUser, botUser],
|
|
@@ -208,6 +214,10 @@ export const ChatBase = () => {
|
|
|
208
214
|
const [msg1, setMsg1] = useState("");
|
|
209
215
|
const [conv1, setConv1] = useState(makeConversation());
|
|
210
216
|
|
|
217
|
+
// INFO: Fixed message box — disableComposerResize
|
|
218
|
+
const [msgFixed, setMsgFixed] = useState("");
|
|
219
|
+
const [convFixed, setConvFixed] = useState(makeConversation());
|
|
220
|
+
|
|
211
221
|
// --- Custom header/footer ---
|
|
212
222
|
const [msg2, setMsg2] = useState("");
|
|
213
223
|
const [conv2, setConv2] = useState(makeConversation());
|
|
@@ -253,7 +263,7 @@ export const ChatBase = () => {
|
|
|
253
263
|
} as any,
|
|
254
264
|
createdAt: dayjs("2023-11-03 12:02").toDate(),
|
|
255
265
|
},
|
|
256
|
-
])
|
|
266
|
+
]),
|
|
257
267
|
);
|
|
258
268
|
const [files4, setFiles4] = useState<File[]>([]);
|
|
259
269
|
|
|
@@ -311,7 +321,7 @@ export const ChatBase = () => {
|
|
|
311
321
|
] as any,
|
|
312
322
|
createdAt: dayjs("2023-11-03 12:02").toDate(),
|
|
313
323
|
},
|
|
314
|
-
])
|
|
324
|
+
]),
|
|
315
325
|
);
|
|
316
326
|
|
|
317
327
|
// --- Loading reply ---
|
|
@@ -333,7 +343,7 @@ export const ChatBase = () => {
|
|
|
333
343
|
const [msgPage, setMsgPage] = useState(1);
|
|
334
344
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
335
345
|
const [conv7, setConv7] = useState<Conversation>(
|
|
336
|
-
makeConversation(generateOlderMessages(0))
|
|
346
|
+
makeConversation(generateOlderMessages(0)),
|
|
337
347
|
);
|
|
338
348
|
|
|
339
349
|
// --- Quick replies (footerElement) ---
|
|
@@ -359,7 +369,7 @@ export const ChatBase = () => {
|
|
|
359
369
|
(
|
|
360
370
|
conv: Conversation,
|
|
361
371
|
setConv: (c: Conversation) => void,
|
|
362
|
-
setMsg: (s: string) => void
|
|
372
|
+
setMsg: (s: string) => void,
|
|
363
373
|
) =>
|
|
364
374
|
(updated: Conversation) => {
|
|
365
375
|
setConv(updated);
|
|
@@ -376,7 +386,7 @@ export const ChatBase = () => {
|
|
|
376
386
|
messageList: conv3.messageList.filter(
|
|
377
387
|
(m) =>
|
|
378
388
|
typeof m.messageBody === "string" &&
|
|
379
|
-
m.messageBody.toLowerCase().includes(searchMessage.toLowerCase())
|
|
389
|
+
m.messageBody.toLowerCase().includes(searchMessage.toLowerCase()),
|
|
380
390
|
),
|
|
381
391
|
});
|
|
382
392
|
}
|
|
@@ -419,7 +429,7 @@ export const ChatBase = () => {
|
|
|
419
429
|
status: "done" as const,
|
|
420
430
|
response: { success: true, output: "OK" },
|
|
421
431
|
}
|
|
422
|
-
: t
|
|
432
|
+
: t,
|
|
423
433
|
),
|
|
424
434
|
};
|
|
425
435
|
msgs[msgs.length - 1] = last;
|
|
@@ -478,7 +488,7 @@ export const ChatBase = () => {
|
|
|
478
488
|
};
|
|
479
489
|
|
|
480
490
|
const clearAction = (
|
|
481
|
-
setConv: (c: Conversation) => void
|
|
491
|
+
setConv: (c: Conversation) => void,
|
|
482
492
|
): ChatBaseAction[] => [
|
|
483
493
|
{
|
|
484
494
|
text: "Clear",
|
|
@@ -491,7 +501,7 @@ export const ChatBase = () => {
|
|
|
491
501
|
<>
|
|
492
502
|
<StorySection
|
|
493
503
|
title="Default"
|
|
494
|
-
description="Basic chat with send and receive. Sent messages appear on the right; received messages on the left with the sender's avatar. Received messages support markdown rendering."
|
|
504
|
+
description="Basic chat with send and receive. Sent messages appear on the right; received messages on the left with the sender's avatar, grouped by sender and split by day. Received messages support markdown rendering. Drag the bar on top of the message box to give it more lines; double-click resets it. Enter sends, Shift+Enter adds a line."
|
|
495
505
|
>
|
|
496
506
|
<ChatBaseImpl
|
|
497
507
|
open
|
|
@@ -506,6 +516,23 @@ export const ChatBase = () => {
|
|
|
506
516
|
/>
|
|
507
517
|
</StorySection>
|
|
508
518
|
|
|
519
|
+
<StorySection
|
|
520
|
+
title="Fixed Message Box"
|
|
521
|
+
description="disableComposerResize removes the drag bar, so the message box keeps its size."
|
|
522
|
+
>
|
|
523
|
+
<ChatBaseImpl
|
|
524
|
+
open
|
|
525
|
+
disableComposerResize
|
|
526
|
+
value={msgFixed}
|
|
527
|
+
currentUser={currentUser}
|
|
528
|
+
conversation={convFixed}
|
|
529
|
+
onSendMessage={sendMessage(convFixed, setConvFixed, setMsgFixed)}
|
|
530
|
+
onChangeMessage={(e) => setMsgFixed(e.target.value)}
|
|
531
|
+
sizeClassName="h-[500px] w-96"
|
|
532
|
+
disableMobileResize
|
|
533
|
+
/>
|
|
534
|
+
</StorySection>
|
|
535
|
+
|
|
509
536
|
<StorySection
|
|
510
537
|
title="Custom Header & Footer"
|
|
511
538
|
description="headerElement replaces the default blue header bar. footerElement renders above the message input — useful for limit warnings, disclaimers, or action prompts."
|
|
@@ -90,10 +90,14 @@ const headerElement = (
|
|
|
90
90
|
| `onSearch` | `(searchMessage: string) => void` | — | Called when the search form is submitted inside `ChatBase` |
|
|
91
91
|
| `onChangeSearch` | `(event: any) => void` | — | Called on every keystroke in the search input inside `ChatBase` |
|
|
92
92
|
| `additionalAction` | `ChatBaseAction[]` | — | Extra items appended to the `⋮` actions menu in the chat panel. Each item requires `text` and `onClick`, with an optional `icon`. |
|
|
93
|
+
| `darkMode` | `boolean` | `false` | Dark surfaces for the conversation list and the chat panel; passed to `ChatBase` |
|
|
94
|
+
| `disableComposerResize` | `boolean` | `false` | Removes `ChatBase`'s drag handle on the message box's top edge — see ChatBase "Message box resize" |
|
|
93
95
|
|
|
94
96
|
##### Conversation list behaviour
|
|
95
97
|
|
|
96
|
-
Each item in `conversationList` renders the other user's avatar and name (the member whose `id` differs from `currentUser.id`) and the last message body
|
|
98
|
+
The list is a `NewList` (`bare`, `activeId` on the selected conversation, `rowIdPrefix="conversation-"`). Each item in `conversationList` renders the other user's avatar (their initials when there is no `imageUrl`) and name (the member whose `id` differs from `currentUser.id`), the time of the last message (`HH:mm` today, `DD MMM` before), and the last message body on one line. Items with a `FileObj` message body show an empty preview string. The selected item has a tinted background and a left accent bar. An empty `conversationList` shows "No conversations yet" unless `isLoadingConversations` is set.
|
|
99
|
+
|
|
100
|
+
The message box's drag-to-resize comes from `ChatBase` — see its "Message box resize" section.
|
|
97
101
|
|
|
98
102
|
##### User
|
|
99
103
|
|
|
@@ -10,14 +10,14 @@ import { ChatDashboard as ChatDashboardImpl } from ".";
|
|
|
10
10
|
import { StorySection } from "../../storyUtils";
|
|
11
11
|
import { IconButton } from "../../action/iconButton";
|
|
12
12
|
import { Loading } from "../../display/loading";
|
|
13
|
-
import { ChatBaseAction, Conversation } from "../chatBase";
|
|
13
|
+
import { ChatBaseAction, Conversation, renderChatAvatar } from "../chatBase";
|
|
14
14
|
import { conversationList as seedConvoList, currentUser } from "../chatUtils";
|
|
15
15
|
import { Button } from "../../action/button";
|
|
16
|
+
import { Tag, TagColor } from "../../display/tag";
|
|
16
17
|
|
|
17
18
|
const meta = { title: "Chat/Chat Dashboard" };
|
|
18
19
|
export default meta;
|
|
19
20
|
|
|
20
|
-
|
|
21
21
|
const sortByLatest = (list: Conversation[]) =>
|
|
22
22
|
[...list].sort((a, b) =>
|
|
23
23
|
dayjs(b.messageList[b.messageList.length - 1].createdAt).diff(
|
|
@@ -27,11 +27,12 @@ const sortByLatest = (list: Conversation[]) =>
|
|
|
27
27
|
|
|
28
28
|
// INFO: demo statuses for renderConversationMeta / renderHeaderTags
|
|
29
29
|
const DEMO_STATUSES = ["active", "waiting", "closed", "assigned"];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// INFO: the tag slots take any element; the demo uses the library's Tag
|
|
31
|
+
const DEMO_BADGE_COLORS: Record<string, TagColor> = {
|
|
32
|
+
active: "green",
|
|
33
|
+
waiting: "yellow",
|
|
34
|
+
closed: "dark",
|
|
35
|
+
assigned: "blue",
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
export const ChatDashboard = () => {
|
|
@@ -69,6 +70,15 @@ export const ChatDashboard = () => {
|
|
|
69
70
|
messageList: [],
|
|
70
71
|
});
|
|
71
72
|
const [paginationMessage, setPaginationMessage] = useState("");
|
|
73
|
+
|
|
74
|
+
// INFO: Section 4 — dark mode, on the seeded conversations
|
|
75
|
+
const [darkConvoList, setDarkConvoList] = useState<Conversation[]>(
|
|
76
|
+
sortByLatest(seedConvoList),
|
|
77
|
+
);
|
|
78
|
+
const [darkConvo, setDarkConvo] = useState<Conversation>(
|
|
79
|
+
sortByLatest(seedConvoList)[0],
|
|
80
|
+
);
|
|
81
|
+
const [darkMessage, setDarkMessage] = useState("");
|
|
72
82
|
const [page, setPage] = useState(1);
|
|
73
83
|
const [loadingConversations, setLoadingConversations] = useState(false);
|
|
74
84
|
const [loadingMessages, setLoadingMessages] = useState(false);
|
|
@@ -118,7 +128,9 @@ export const ChatDashboard = () => {
|
|
|
118
128
|
token: img.label,
|
|
119
129
|
type: "image/jpeg",
|
|
120
130
|
},
|
|
121
|
-
createdAt: dayjs()
|
|
131
|
+
createdAt: dayjs()
|
|
132
|
+
.subtract(demoImages.length - img.id, "minutes")
|
|
133
|
+
.toDate(),
|
|
122
134
|
})),
|
|
123
135
|
{
|
|
124
136
|
sender: convo.members.find((m) => m.id !== currentUser.id)!,
|
|
@@ -212,7 +224,7 @@ export const ChatDashboard = () => {
|
|
|
212
224
|
{/* INFO: Section 1 — Base chat with file upload, search, actions */}
|
|
213
225
|
<StorySection
|
|
214
226
|
title="Base Chat"
|
|
215
|
-
description="Two-panel layout with conversation list, file upload, search, and actions. Mobile toggle via mobileSelectedChat."
|
|
227
|
+
description="Two-panel layout with conversation list, file upload, search, and actions. Drag the bar on top of the message box to give it more lines (double-click resets it). Mobile toggle via mobileSelectedChat."
|
|
216
228
|
>
|
|
217
229
|
<div className="w-full">
|
|
218
230
|
<ChatDashboardImpl
|
|
@@ -225,14 +237,15 @@ export const ChatDashboard = () => {
|
|
|
225
237
|
sizeClassName="w-96 h-[60vh]"
|
|
226
238
|
chatBaseSizeClassName="w-full h-[60vh]"
|
|
227
239
|
headerElement={
|
|
228
|
-
<div className="w-full flex items-center h-
|
|
240
|
+
<div className="w-full flex items-center gap-2 h-14 px-2 sm:px-4">
|
|
229
241
|
<IconButton
|
|
230
242
|
icon={<ChevronLeftIcon className="w-6 h-6" />}
|
|
231
|
-
background="bg-transparent"
|
|
243
|
+
background="bg-transparent text-gray-500 hover:bg-gray-100"
|
|
232
244
|
onClick={() => setMobileSelectedChat(false)}
|
|
233
245
|
className={`${!mobileSelectedChat ? "hidden sm:flex" : ""} sm:hidden`}
|
|
234
246
|
/>
|
|
235
|
-
|
|
247
|
+
{renderChatAvatar(otherUser, "h-9 w-9")}
|
|
248
|
+
<p className="text-sm font-semibold text-gray-900">
|
|
236
249
|
{otherUser?.name ?? "Chat"}
|
|
237
250
|
</p>
|
|
238
251
|
</div>
|
|
@@ -299,8 +312,12 @@ export const ChatDashboard = () => {
|
|
|
299
312
|
sizeClassName="w-96 h-[60vh]"
|
|
300
313
|
chatBaseSizeClassName="w-full h-[60vh]"
|
|
301
314
|
headerElement={
|
|
302
|
-
<div className="w-full flex items-center h-
|
|
303
|
-
|
|
315
|
+
<div className="w-full flex items-center gap-3 h-14 px-4">
|
|
316
|
+
{renderChatAvatar(
|
|
317
|
+
metaConvo.members.find((m) => m.id !== currentUser.id),
|
|
318
|
+
"h-9 w-9",
|
|
319
|
+
)}
|
|
320
|
+
<p className="text-sm font-semibold text-gray-900">
|
|
304
321
|
{metaConvo.members.find((m) => m.id !== currentUser.id)
|
|
305
322
|
?.name ?? "Chat"}
|
|
306
323
|
</p>
|
|
@@ -313,16 +330,22 @@ export const ChatDashboard = () => {
|
|
|
313
330
|
];
|
|
314
331
|
const needsReply = convo.id.charCodeAt(0) % 2 === 0;
|
|
315
332
|
return (
|
|
316
|
-
<div className="flex gap-1
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
333
|
+
<div className="flex gap-1 flex-wrap">
|
|
334
|
+
<Tag
|
|
335
|
+
tagText={status}
|
|
336
|
+
size="small"
|
|
337
|
+
variant="flat"
|
|
338
|
+
rounded="md"
|
|
339
|
+
color={DEMO_BADGE_COLORS[status]}
|
|
340
|
+
/>
|
|
322
341
|
{needsReply && (
|
|
323
|
-
<
|
|
324
|
-
Needs Reply
|
|
325
|
-
|
|
342
|
+
<Tag
|
|
343
|
+
tagText="Needs Reply"
|
|
344
|
+
size="small"
|
|
345
|
+
variant="flat"
|
|
346
|
+
rounded="md"
|
|
347
|
+
color="orange"
|
|
348
|
+
/>
|
|
326
349
|
)}
|
|
327
350
|
</div>
|
|
328
351
|
);
|
|
@@ -334,12 +357,20 @@ export const ChatDashboard = () => {
|
|
|
334
357
|
];
|
|
335
358
|
return (
|
|
336
359
|
<div className="flex gap-1">
|
|
337
|
-
<
|
|
338
|
-
{status}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
360
|
+
<Tag
|
|
361
|
+
tagText={status}
|
|
362
|
+
size="small"
|
|
363
|
+
variant="flat"
|
|
364
|
+
rounded="md"
|
|
365
|
+
color={DEMO_BADGE_COLORS[status]}
|
|
366
|
+
/>
|
|
367
|
+
<Tag
|
|
368
|
+
tagText="whatsapp"
|
|
369
|
+
size="small"
|
|
370
|
+
variant="flat"
|
|
371
|
+
rounded="md"
|
|
372
|
+
color="green"
|
|
373
|
+
/>
|
|
343
374
|
</div>
|
|
344
375
|
);
|
|
345
376
|
}}
|
|
@@ -377,11 +408,14 @@ export const ChatDashboard = () => {
|
|
|
377
408
|
sizeClassName="w-96 h-[60vh]"
|
|
378
409
|
chatBaseSizeClassName="w-full h-[60vh]"
|
|
379
410
|
headerElement={
|
|
380
|
-
<div className="w-full flex items-center h-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
411
|
+
<div className="w-full flex items-center gap-3 h-14 px-4">
|
|
412
|
+
{renderChatAvatar(
|
|
413
|
+
paginationConvo.members.find((m) => m.id !== currentUser.id),
|
|
414
|
+
"h-9 w-9",
|
|
415
|
+
)}
|
|
416
|
+
<p className="text-sm font-semibold text-gray-900">
|
|
417
|
+
{paginationConvo.members.find((m) => m.id !== currentUser.id)
|
|
418
|
+
?.name ?? "Chat"}
|
|
385
419
|
</p>
|
|
386
420
|
</div>
|
|
387
421
|
}
|
|
@@ -405,9 +439,7 @@ export const ChatDashboard = () => {
|
|
|
405
439
|
...Array.from({ length: 10 }, (_, i) => ({
|
|
406
440
|
sender:
|
|
407
441
|
i % 2 === 0
|
|
408
|
-
? prev.members.find(
|
|
409
|
-
(m) => m.id !== currentUser.id,
|
|
410
|
-
)!
|
|
442
|
+
? prev.members.find((m) => m.id !== currentUser.id)!
|
|
411
443
|
: currentUser,
|
|
412
444
|
messageBody: `Older message ${i + 1} from page ${nextMsgPage}`,
|
|
413
445
|
createdAt: dayjs()
|
|
@@ -438,6 +470,38 @@ export const ChatDashboard = () => {
|
|
|
438
470
|
/>
|
|
439
471
|
</div>
|
|
440
472
|
</StorySection>
|
|
473
|
+
|
|
474
|
+
{/* INFO: Section 4 — darkMode on the list, the thread and the message box */}
|
|
475
|
+
<StorySection
|
|
476
|
+
title="Dark Mode"
|
|
477
|
+
description="darkMode styles the conversation list, the thread and the message box. The drag handle on the message box works the same way."
|
|
478
|
+
>
|
|
479
|
+
<div className="w-full rounded-2xl bg-black p-4">
|
|
480
|
+
<ChatDashboardImpl
|
|
481
|
+
darkMode
|
|
482
|
+
value={darkMessage}
|
|
483
|
+
currentUser={currentUser}
|
|
484
|
+
selectedConversation={darkConvo}
|
|
485
|
+
conversationList={darkConvoList}
|
|
486
|
+
sizeClassName="w-96 h-[60vh]"
|
|
487
|
+
chatBaseSizeClassName="w-full h-[60vh]"
|
|
488
|
+
onChangeMessage={(e) => setDarkMessage(e.target.value)}
|
|
489
|
+
onSendMessage={(updated) => {
|
|
490
|
+
setDarkConvo(updated);
|
|
491
|
+
setDarkMessage("");
|
|
492
|
+
setDarkConvoList((prev) =>
|
|
493
|
+
sortByLatest(
|
|
494
|
+
prev.map((c) => (c.id === updated.id ? updated : c)),
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
}}
|
|
498
|
+
handleSelectedConversation={(id) => {
|
|
499
|
+
const found = darkConvoList.find((c) => c.id === id);
|
|
500
|
+
if (found) setDarkConvo(found);
|
|
501
|
+
}}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
</StorySection>
|
|
441
505
|
</>
|
|
442
506
|
);
|
|
443
|
-
}
|
|
507
|
+
};
|
|
@@ -17,6 +17,7 @@ Mobile-native list foundation. Other components (`NewFeeds`, `NewTable` mobile b
|
|
|
17
17
|
## Foundation status
|
|
18
18
|
|
|
19
19
|
This is the **base primitive** for the Batch 3 work. `NewListRow` is exported intentionally so:
|
|
20
|
+
|
|
20
21
|
- `NewFeeds` can use it with `renderExtra` to attach quoted content + reactions
|
|
21
22
|
- `NewTable` mobile breakpoint can use it instead of inventing a card-per-row layout
|
|
22
23
|
|
|
@@ -24,15 +25,15 @@ Do not duplicate the row layout in those components — extend `NewListRow` via
|
|
|
24
25
|
|
|
25
26
|
## Item content
|
|
26
27
|
|
|
27
|
-
| Item field
|
|
28
|
-
|
|
|
29
|
-
| `title`
|
|
30
|
-
| `description`
|
|
31
|
-
| `tag` / `tags`
|
|
32
|
-
| `date`
|
|
33
|
-
| `icon` / `img`
|
|
34
|
-
| `action`
|
|
35
|
-
| any key in `extraKeys` | Block content under the description, in order
|
|
28
|
+
| Item field | Rendered as |
|
|
29
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
30
|
+
| `title` | Row title |
|
|
31
|
+
| `description` | String / number: a `<p>`. Element: a `<div>` (block content). Both are truncated to one line per line of content unless `noTruncate` |
|
|
32
|
+
| `tag` / `tags` | Pill(s) next to the title |
|
|
33
|
+
| `date` | Right-aligned date text (see below) |
|
|
34
|
+
| `icon` / `img` | Avatar |
|
|
35
|
+
| `action` | Right-side element |
|
|
36
|
+
| any key in `extraKeys` | Block content under the description, in order |
|
|
36
37
|
|
|
37
38
|
The legacy `List` renders **every** extra key on an item. `NewList` renders only the keys listed in `extraKeys`:
|
|
38
39
|
|
|
@@ -42,10 +43,10 @@ The legacy `List` renders **every** extra key on an item. `NewList` renders only
|
|
|
42
43
|
|
|
43
44
|
## Date
|
|
44
45
|
|
|
45
|
-
| `date` value
|
|
46
|
-
|
|
|
47
|
-
| `Date`, or a string / number `new Date()` parses
|
|
48
|
-
| A string `new Date()` cannot parse (e.g. `"18/09/2026 10:00"`) | The string as given
|
|
46
|
+
| `date` value | Output |
|
|
47
|
+
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
48
|
+
| `Date`, or a string / number `new Date()` parses | `dateFormat(date)` when given, else `date.toLocaleDateString()` (browser locale, no time) |
|
|
49
|
+
| A string `new Date()` cannot parse (e.g. `"18/09/2026 10:00"`) | The string as given |
|
|
49
50
|
|
|
50
51
|
```tsx
|
|
51
52
|
<NewList
|
|
@@ -54,6 +55,17 @@ The legacy `List` renders **every** extra key on an item. `NewList` renders only
|
|
|
54
55
|
/>
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
## Active row, row ids, empty text, bare
|
|
59
|
+
|
|
60
|
+
| Prop | Effect |
|
|
61
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `activeId` | The item with this `id` gets the selected tint and a left accent bar. Separate from checkbox `selectedIds` |
|
|
63
|
+
| `rowIdPrefix` | Each row gets `id` and `data-testid` of `${rowIdPrefix}${item.id}` |
|
|
64
|
+
| `emptyText` | Replaces "There is no data available at the moment" when the list is empty |
|
|
65
|
+
| `bare` | No card border, rounding, shadow or background, and no row dividers — for a list inside an already framed panel |
|
|
66
|
+
|
|
67
|
+
ChatDashboard's conversation list uses all four.
|
|
68
|
+
|
|
57
69
|
## Status
|
|
58
70
|
|
|
59
71
|
🚧 **Scaffold only.** Implementation lands in follow-up commits per #1714.
|
|
@@ -162,6 +162,7 @@ On error the editor draws a red outline (`border border-red-500 rounded`) — th
|
|
|
162
162
|
| `compress` | `{ maxFileSize?: number, fileQuality?: number}` | `null` | Image compression settings. `maxFileSize` in MB (default: 30), `fileQuality` 0.0-1.0 (default: 0.8) |
|
|
163
163
|
| `lib` | `{ axios?: any, mermaid?: () => Promise<any> }` | `null` | External libraries — axios for API calls; `mermaid` loader (e.g. `() => import("mermaid")`) to draw diagrams |
|
|
164
164
|
| `uploadEndpoint` | `string` | `null` | API endpoint for uploading media files |
|
|
165
|
+
| `onHeadingsChange` | `(headings: MarkdownHeading[]) => void` | `null` | Reports the document's `h1`–`h3` outline whenever it changes — feed it to `MarkdownOutline` |
|
|
165
166
|
|
|
166
167
|
##### Ref Methods
|
|
167
168
|
|
|
@@ -260,6 +261,85 @@ the block. The `/` menu gains **Diagram**, which inserts a starter flowchart.
|
|
|
260
261
|
- **The source is what is stored**, as a normal fenced block, so search and
|
|
261
262
|
anything reading the raw markdown still see the diagram's text.
|
|
262
263
|
|
|
264
|
+
##### Heading ids
|
|
265
|
+
|
|
266
|
+
Both renderers give `h1`–`h3` a slug id and report the outline through
|
|
267
|
+
`onHeadingsChange`, so a host can build its own navigation — a sticky rail, tabs,
|
|
268
|
+
a jump menu — without parsing the markdown itself or reaching into the rendered
|
|
269
|
+
DOM to find anchors.
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
const [headings, setHeadings] = useState<MarkdownHeading[]>([]);
|
|
273
|
+
|
|
274
|
+
<MarkdownInput value={body} onChange={handleChange} onHeadingsChange={setHeadings} />;
|
|
275
|
+
|
|
276
|
+
// each entry is { id, level, text }; the id is on the rendered heading
|
|
277
|
+
document.getElementById(headings[0].id)?.scrollIntoView({ behavior: "smooth" });
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
What the rail looks like is deliberately left out of this component — an outline in
|
|
281
|
+
a dashboard's own chrome is that app's design problem, and the only thing it needs
|
|
282
|
+
from here is where the sections are.
|
|
283
|
+
|
|
284
|
+
- **Ids are derived, never stored.** Slugs of the heading text, computed at
|
|
285
|
+
render, so a document written before any of this existed gets them with no
|
|
286
|
+
migration and no change to a single stored byte.
|
|
287
|
+
- **Each renderer carries the id the way its own DOM survives.** `MarkdownStatic`
|
|
288
|
+
injects it into the html string, because React re-commits that string on
|
|
289
|
+
re-render and wipes anything written to the mounted nodes afterwards. The editor
|
|
290
|
+
uses a ProseMirror decoration, which is rebuilt from the document on every
|
|
291
|
+
redraw and never enters the document — a node attribute would reach
|
|
292
|
+
`getHTML()`, which is turndown's input, and a DOM write is undone on the next
|
|
293
|
+
redraw.
|
|
294
|
+
- **One parser for both.** The editor and `MarkdownStatic` draw headings through
|
|
295
|
+
completely different paths, and an outline read from either would only be right
|
|
296
|
+
there. Both are labelled against the markdown instead, so a note and the page
|
|
297
|
+
published from it can never list different sections.
|
|
298
|
+
- **Fenced blocks are skipped**, so a `#` comment in a ```` ```bash ```` block or
|
|
299
|
+
a ```` ```mermaid ```` diagram is not mistaken for a heading. A closing fence
|
|
300
|
+
must be at least as long as the one that opened it.
|
|
301
|
+
- **Repeated headings get distinct ids** — `overview`, `overview-2`. Two
|
|
302
|
+
"Overview" sections in a long page are ordinary, and sharing an id would send
|
|
303
|
+
every click to the first.
|
|
304
|
+
|
|
305
|
+
##### Resizing an image
|
|
306
|
+
|
|
307
|
+
Hover or select an image in the editor and drag the handle on either edge. A
|
|
308
|
+
double click returns it to its natural size.
|
|
309
|
+
|
|
310
|
+
- **Only a width is stored, never a height.** The image keeps its own aspect
|
|
311
|
+
ratio, which is what makes one axis enough and makes it impossible to save a
|
|
312
|
+
distorted image.
|
|
313
|
+
- **A resized image is stored as html**, `<img src="…" alt="…" width="420" />`,
|
|
314
|
+
because markdown has no syntax for a size — the same route `<video>`, the file
|
|
315
|
+
attachment and the space embed take, and markdown-it runs with `html: true` so
|
|
316
|
+
it parses back on load. An image nobody resized still serialises as
|
|
317
|
+
``, so every document written before this existed round-trips
|
|
318
|
+
byte-for-byte.
|
|
319
|
+
- **The handle starts the drag in the capture phase.** ProseMirror handles pointer
|
|
320
|
+
events on the editor element to move the selection and to drag a node around the
|
|
321
|
+
document, and its listener sits on an ancestor of the handle — so a bubble-phase
|
|
322
|
+
handler never runs at all.
|
|
323
|
+
- **The document is edited once per drag, on release.** Writing the width on
|
|
324
|
+
every pointer move would put one transaction per mouse move into the history —
|
|
325
|
+
undo would take fifty presses — and fire the host's autosave just as often.
|
|
326
|
+
- **Capped at the width of the column** it sits in — measured from the editor
|
|
327
|
+
element, not from the node view wrapper, which is `fit-content` and therefore
|
|
328
|
+
exactly the image's current width.
|
|
329
|
+
- **`MarkdownStatic` needs nothing.** The width attribute is html a browser
|
|
330
|
+
already honours, under the `max-width: 100%` prose already sets, so a published
|
|
331
|
+
page shows the size the author chose and still fits a phone.
|
|
332
|
+
- **Works on a touch screen.** Two things are load-bearing and neither is
|
|
333
|
+
obvious: `touch-action: none` on the handle, or the browser claims the gesture
|
|
334
|
+
as a scroll and cancels the pointer stream a couple of dozen pixels in — which
|
|
335
|
+
reads as the handle being unresponsive rather than as a scroll — and a hit area
|
|
336
|
+
far larger than the 6px bar it draws. The drag also takes pointer capture, so
|
|
337
|
+
it keeps tracking after the finger leaves the handle, and a touch on the image
|
|
338
|
+
reveals the handles, since a phone never fires hover.
|
|
339
|
+
- **The tiptap extension's own `resize` option is deliberately not used.** It
|
|
340
|
+
commits a height as well as a width, which is a distorted image waiting to
|
|
341
|
+
happen, and it hides the image until `onload` fires.
|
|
342
|
+
|
|
263
343
|
##### The slash menu
|
|
264
344
|
|
|
265
345
|
Only a real edit opens it. The detection also runs on selection changes so that
|
|
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|
|
2
2
|
import { MarkdownInput as MarkdownInputImpl, MarkdownStatic } from ".";
|
|
3
3
|
import { StorySection } from "../../storyUtils";
|
|
4
4
|
|
|
5
|
-
const meta = { title: "Input/Markdown
|
|
5
|
+
const meta = { title: "Input/Markdown/Input" };
|
|
6
6
|
export default meta;
|
|
7
7
|
|
|
8
8
|
// INFO: the host passes mermaid in; the library never imports it
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { MarkdownInput, MarkdownStatic } from ".";
|
|
3
|
+
import { StorySection } from "../../storyUtils";
|
|
4
|
+
|
|
5
|
+
const meta = { title: "Input/Markdown/Image Resize" };
|
|
6
|
+
export default meta;
|
|
7
|
+
|
|
8
|
+
// INFO: a neutral remote placeholder — nothing here points at a project bucket
|
|
9
|
+
const IMAGE = "https://placehold.co/900x500/png";
|
|
10
|
+
|
|
11
|
+
const SIZED = [
|
|
12
|
+
"An image nobody has touched is stored as plain markdown:",
|
|
13
|
+
"",
|
|
14
|
+
``,
|
|
15
|
+
"",
|
|
16
|
+
"One that has been resized carries its width as html:",
|
|
17
|
+
"",
|
|
18
|
+
`<img src="${IMAGE}" alt="A wide placeholder" width="320" />`,
|
|
19
|
+
].join("\n");
|
|
20
|
+
|
|
21
|
+
export const Editor = () => {
|
|
22
|
+
const [value, setValue] = useState<string | undefined>(SIZED);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<StorySection
|
|
27
|
+
title="Drag to resize"
|
|
28
|
+
description="Hover or select an image and drag the handle on either edge. Double click returns it to its natural size. The width is previewed while dragging and written to the document once, on release — watch the markdown below, which only changes when the drag ends."
|
|
29
|
+
>
|
|
30
|
+
<div className="w-full">
|
|
31
|
+
<MarkdownInput
|
|
32
|
+
id="markdown"
|
|
33
|
+
value={value ?? ""}
|
|
34
|
+
placeholder="Press / to see the available commands"
|
|
35
|
+
borderless={true}
|
|
36
|
+
onChange={(_id: string, next?: string) => setValue(next)}
|
|
37
|
+
enableImageUpload={true}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
</StorySection>
|
|
41
|
+
<StorySection
|
|
42
|
+
title="Stored markdown"
|
|
43
|
+
description="A resized image is html because markdown has no syntax for a size; an untouched one stays , so documents written before resizing existed serialise exactly as they did. Only a width is ever stored — the image keeps its own aspect ratio."
|
|
44
|
+
>
|
|
45
|
+
<pre
|
|
46
|
+
data-testid="stored-markdown"
|
|
47
|
+
className="w-full overflow-x-auto rounded bg-gray-50 p-3 text-xs text-gray-700"
|
|
48
|
+
>
|
|
49
|
+
{value}
|
|
50
|
+
</pre>
|
|
51
|
+
</StorySection>
|
|
52
|
+
<StorySection
|
|
53
|
+
title="Read-only"
|
|
54
|
+
description="The same document with no editor. The width attribute is html a browser already honours under the max-width prose sets, so a published page shows the size the author chose and still fits a phone."
|
|
55
|
+
>
|
|
56
|
+
<MarkdownStatic label="" value={value ?? ""} />
|
|
57
|
+
</StorySection>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
};
|