blixify-ui-web-mcp 0.1.26 → 0.1.28

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.
@@ -88,25 +88,27 @@ return (
88
88
 
89
89
  ##### Optional props — inherited from ChatBase
90
90
 
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) |
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 | 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 blue "Chat" header bar |
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 |
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 { ChatBase as ChatBaseImpl, ChatBaseAction, Conversation, Message, ToolCall } from ".";
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 as a preview. Items with a `FileObj` message body show an empty preview string.
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
- const DEMO_BADGE_COLORS: Record<string, string> = {
31
- active: "bg-green-100 text-green-700",
32
- waiting: "bg-yellow-100 text-yellow-700",
33
- closed: "bg-gray-100 text-gray-500",
34
- assigned: "bg-blue-100 text-blue-700",
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().subtract(demoImages.length - img.id, "minutes").toDate(),
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-12 text-white bg-primary-500">
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
- <p className="ml-2 sm:ml-4 font-medium">
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-12 px-4 text-white bg-primary-500">
303
- <p className="font-medium">
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 mt-1 flex-wrap">
317
- <span
318
- className={`text-[10px] px-1.5 py-0.5 rounded ${DEMO_BADGE_COLORS[status]}`}
319
- >
320
- {status}
321
- </span>
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
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-700">
324
- Needs Reply
325
- </span>
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
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-white/20 text-white">
338
- {status}
339
- </span>
340
- <span className="text-[10px] px-1.5 py-0.5 rounded bg-white/20 text-white">
341
- whatsapp
342
- </span>
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-12 px-4 text-white bg-primary-500">
381
- <p className="font-medium">
382
- {paginationConvo.members.find(
383
- (m) => m.id !== currentUser.id,
384
- )?.name ?? "Chat"}
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
+ };
@@ -212,6 +212,26 @@ Hide and reorder are **per user** by default, kept in `persistViewState`. Wire
212
212
  the change is written to the model and handed to the consumer to persist for everyone — which
213
213
  is what a Space-backed DTW wants.
214
214
 
215
+ Persist the single change, not the list. `onChangeHide` passes the columns as shown plus the
216
+ edit that was made, and `onChangeDrag` passes the two column ids:
217
+
218
+ ```tsx
219
+ organise={{
220
+ onChangeHide: (_columns, change) => {
221
+ // INFO: apply the one edit to the stored columns — the on-screen list may carry a
222
+ // saved view, another tab's state or a copy older than the stored one
223
+ const next =
224
+ "showAll" in change
225
+ ? stored.map((each) => ({ ...each, hide: false }))
226
+ : stored.map((each) =>
227
+ each.id === change.columnId ? { ...each, hide: change.hide } : each,
228
+ );
229
+ handleSaveColumns(next);
230
+ },
231
+ onChangeDrag: (dragId, dropId) => handleSaveColumns(handleMove(stored, dragId, dropId)),
232
+ }}
233
+ ```
234
+
215
235
  ## Step-by-Step Setup
216
236
 
217
237
  **1. Install peer dependencies**
@@ -986,8 +1006,8 @@ Which field types can be edited from the list table, and how. Types not listed u
986
1006
  | `addColumnEnabled` | `boolean` | Shows an add-column button in the table header |
987
1007
  | `persistViewState` | `"persistent" \| "session"` | Remembers the list's sort, filters, hidden columns, page, rows per page, search and reading position per collection, so a Back navigation lands where the user left. `"persistent"` uses `localStorage`; `"session"` writes `sessionStorage` and reads it first, falling back to `localStorage`. The reading position is restored on mount and on returning from a record, not on a tab switch; with several lists on one page, only the list a record was last opened from restores (tracked in `blixify-dtw-persist-last-drill`). Covers table, list, grid, kanban and feeds; not map. List view only |
988
1008
  | `persistKey` | `string` | Keeps a separate saved view state for this instance: the storage key becomes `blixify-dtw-persist-<collectionId>-<persistKey>`. Saved state is read once, on mount, so give the component a React `key` that changes with `persistKey` |
989
- | `savedViews` | `boolean` | Shows a row of view tags under the tab bar: save the current filters, flex filters, sort, rows per page and columns under a name, reapply it, update it or delete it. Views belong to the tab they were saved on and are kept in `localStorage` under `blixify-dtw-views-<collectionId>[-<persistKey>][-<currentUser.userEmail>]`. Sort is held by the template while it is on, as with `persistViewState` |
990
- | `defaultSavedViews` | `DefaultSavedView[]` | Views provided by the app (`{ id?, name, tabId?, state }`), listed before the user's own and never deleted. Without `tabId` a view is listed on every tab. A field left out of `state` is reset to its default when the view is applied |
1009
+ | `savedViews` | `boolean` | Shows a row of view tags under the tab bar: save the current filters, flex filters, sort, rows per page and columns under a name, reapply it, update it or delete it. Views belong to the tab they were saved on and are kept in `localStorage` under `blixify-dtw-views-<collectionId>[-<persistKey>][-<currentUser.userEmail>]`. Sort is held by the template while it is on, as with `persistViewState`, and applying a view calls `onChangeSort`, `onChangePagination(0)` and `bareListHandleFilter` when those change. Each write re-reads storage first, so views saved in another browser tab are kept. With `onChangeHide` / `onChangeDrag`, a view's hides and column order stay on screen only: a toggle or reorder after applying one writes back the schema's columns plus that single change |
1010
+ | `defaultSavedViews` | `DefaultSavedView[]` | Views provided by the app (`{ id?, name, tabId?, state }`), listed before the user's own and never deleted. Without `tabId` a view is listed on every tab. A field left out of `state` is reset to its default when the view is applied. `id` must be unique; without one the view gets `default-<index>`, its position in the whole array |
991
1011
  | `groupBy` | `string` | Column ID to group list results by |
992
1012
  | `groupIdCollapsible` | `{ [key: string]: boolean }` | Initial collapsed/expanded state per group ID |
993
1013
  | `sortGroupByAlphabetically` | `boolean` | Sorts group-by values alphabetically |
@@ -995,7 +1015,7 @@ Which field types can be edited from the list table, and how. Types not listed u
995
1015
  | `moreSettings` | `{ text: string; onClick: () => void }[]` | Extra items appended to the "more" dropdown menu |
996
1016
  | `exportSettings` | `ExportSettings` | See Export Settings below |
997
1017
  | `onChangeSort` | `(value: Sort) => void` | Called when the user changes the sort |
998
- | `onChangeHide` | `(value: DataStructure[]) => void` | Called when a column is hidden from the view settings panel. Providing it switches the panel to schema mode — the hide is written to the model for everyone instead of being this user's own |
1018
+ | `onChangeHide` | `(value: DataStructure[], change: HideChange) => void` | Called when a column is hidden or shown from the view settings panel. Providing it switches the panel to schema mode — the hide is written to the model for everyone instead of being this user's own. `change` is `{ columnId, hide }` for one column or `{ showAll: true }`; persist that rather than `value` |
999
1019
  | `onChangeDrag` | `(dragId: string, dropId: string, newHeader: TableHeader[]) => void` | Called after a column is reordered in the view settings panel, so the consumer can persist the order |
1000
1020
  | `onChangeTabs` | `(value: ExtendedTabs[]) => void` | Called when tabs are added/removed/reordered |
1001
1021
  | `onChangePagination` | `(page: number) => void` | Called when the page changes |