@natoe/colab 0.1.12 → 0.1.14

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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import React4, { createContext, forwardRef, useRef, useImperativeHandle, useEffect, useCallback, useState, useLayoutEffect, useContext, useId, useMemo } from 'react';
1
+ import React4, { createContext, forwardRef, useRef, useImperativeHandle, useLayoutEffect, useEffect, useCallback, useState, useContext, useId, useMemo } from 'react';
2
2
  import { Socket, Presence } from 'phoenix';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import { createPortal } from 'react-dom';
@@ -60,6 +60,13 @@ function toCamelKey(key) {
60
60
  var CollabSocket = class {
61
61
  constructor() {
62
62
  this.socket = null;
63
+ /**
64
+ * Conversation channels keyed by id. Each entry is reference-counted so
65
+ * multiple surfaces (e.g. inline chat + expanded panel mounted at once
66
+ * for the same conversation) can coexist without one's `leaveConversation`
67
+ * tearing the channel out from under the other. See `joinConversation`
68
+ * and the returned `ChannelSubscription.release`.
69
+ */
63
70
  this.channels = /* @__PURE__ */ new Map();
64
71
  this.presences = /* @__PURE__ */ new Map();
65
72
  this.userChannel = null;
@@ -130,59 +137,85 @@ var CollabSocket = class {
130
137
  onUnreadCountUpdate(callback) {
131
138
  this.onUnreadUpdate = callback;
132
139
  }
133
- /** Join a conversation channel and subscribe to events */
140
+ /**
141
+ * Join a conversation channel and subscribe to events.
142
+ *
143
+ * Reference-counted: multiple callers can join the same conversation
144
+ * (e.g. inline preview + expanded panel mounted side-by-side). Each
145
+ * call binds its own listeners and gets back a `ChannelSubscription`.
146
+ * The underlying channel only `.leave()`s the server when the LAST
147
+ * subscriber calls `release()`.
148
+ */
134
149
  joinConversation(conversationId, callbacks) {
135
150
  if (!this.socket) return null;
136
- if (this.channels.has(conversationId)) {
137
- return this.channels.get(conversationId);
151
+ let entry = this.channels.get(conversationId);
152
+ if (!entry) {
153
+ const channel2 = this.socket.channel(`conversation:${conversationId}`, {});
154
+ channel2.join().receive("ok", () => {
155
+ }).receive("error", (reason) => {
156
+ this.config?.onError?.({
157
+ code: "CHANNEL_JOIN_ERROR",
158
+ message: `Failed to join conversation ${conversationId}`,
159
+ details: reason
160
+ });
161
+ });
162
+ entry = { channel: channel2, subscribers: 0, presence: null };
163
+ this.channels.set(conversationId, entry);
138
164
  }
139
- const channel = this.socket.channel(`conversation:${conversationId}`, {});
165
+ const channel = entry.channel;
166
+ const refs = [];
167
+ const bind = (event, fn) => {
168
+ const ref = channel.on(event, fn);
169
+ refs.push({ event, ref });
170
+ };
140
171
  if (callbacks.onMessage) {
141
- channel.on(EVENTS.MESSAGE_NEW, (payload) => {
172
+ bind(EVENTS.MESSAGE_NEW, (payload) => {
142
173
  callbacks.onMessage(snakeToCamel(payload));
143
174
  });
144
175
  }
145
176
  if (callbacks.onTyping) {
146
- channel.on(EVENTS.USER_TYPING, (payload) => {
177
+ bind(EVENTS.USER_TYPING, (payload) => {
147
178
  callbacks.onTyping(snakeToCamel(payload));
148
179
  });
149
180
  }
150
181
  if (callbacks.onUserJoined) {
151
- channel.on(EVENTS.USER_JOINED, (payload) => {
182
+ bind(EVENTS.USER_JOINED, (payload) => {
152
183
  callbacks.onUserJoined(snakeToCamel(payload));
153
184
  });
154
185
  }
155
186
  if (callbacks.onUserLeft) {
156
- channel.on(EVENTS.USER_LEFT, (payload) => {
187
+ bind(EVENTS.USER_LEFT, (payload) => {
157
188
  callbacks.onUserLeft(snakeToCamel(payload));
158
189
  });
159
190
  }
160
191
  if (callbacks.onChannelUpdated) {
161
- channel.on(EVENTS.CHANNEL_UPDATED, (payload) => {
192
+ bind(EVENTS.CHANNEL_UPDATED, (payload) => {
162
193
  callbacks.onChannelUpdated(snakeToCamel(payload));
163
194
  });
164
195
  }
165
196
  if (callbacks.onChannelDeleted) {
166
- channel.on(EVENTS.CHANNEL_DELETED, () => {
197
+ bind(EVENTS.CHANNEL_DELETED, () => {
167
198
  callbacks.onChannelDeleted();
168
199
  });
169
200
  }
170
201
  if (callbacks.onMessageRead) {
171
- channel.on(EVENTS.MESSAGE_READ, (payload) => {
172
- callbacks.onMessageRead(snakeToCamel(payload));
202
+ bind(EVENTS.MESSAGE_READ, (payload) => {
203
+ callbacks.onMessageRead(
204
+ snakeToCamel(payload)
205
+ );
173
206
  });
174
207
  }
175
208
  if (callbacks.onMessagePinned) {
176
- channel.on(EVENTS.MESSAGE_PINNED, (payload) => {
209
+ bind(EVENTS.MESSAGE_PINNED, (payload) => {
177
210
  callbacks.onMessagePinned(snakeToCamel(payload));
178
211
  });
179
212
  }
180
213
  if (callbacks.onMessageUnpinned) {
181
- channel.on(EVENTS.MESSAGE_UNPINNED, (payload) => {
214
+ bind(EVENTS.MESSAGE_UNPINNED, (payload) => {
182
215
  callbacks.onMessageUnpinned(snakeToCamel(payload));
183
216
  });
184
217
  }
185
- if (callbacks.onPresence) {
218
+ if (callbacks.onPresence && !entry.presence) {
186
219
  const presence = new Presence(channel);
187
220
  presence.onSync(() => {
188
221
  const online = {};
@@ -191,27 +224,45 @@ var CollabSocket = class {
191
224
  });
192
225
  callbacks.onPresence(online);
193
226
  });
227
+ entry.presence = presence;
194
228
  this.presences.set(conversationId, presence);
195
229
  }
196
- channel.join().receive("ok", () => {
197
- }).receive("error", (reason) => {
198
- this.config?.onError?.({
199
- code: "CHANNEL_JOIN_ERROR",
200
- message: `Failed to join conversation ${conversationId}`,
201
- details: reason
202
- });
203
- });
204
- this.channels.set(conversationId, channel);
205
- return channel;
230
+ entry.subscribers += 1;
231
+ let released = false;
232
+ const release = () => {
233
+ if (released) return;
234
+ released = true;
235
+ const current = this.channels.get(conversationId);
236
+ if (!current) return;
237
+ for (const { event, ref } of refs) {
238
+ current.channel.off(event, ref);
239
+ }
240
+ current.subscribers -= 1;
241
+ if (current.subscribers <= 0) {
242
+ current.channel.leave();
243
+ this.channels.delete(conversationId);
244
+ this.presences.delete(conversationId);
245
+ }
246
+ };
247
+ return { channel, release };
206
248
  }
207
- /** Leave a conversation channel */
208
- leaveConversation(conversationId) {
209
- const channel = this.channels.get(conversationId);
210
- if (channel) {
211
- channel.leave();
212
- this.channels.delete(conversationId);
213
- this.presences.delete(conversationId);
214
- }
249
+ /**
250
+ * @deprecated Use the `release()` method returned by `joinConversation()`.
251
+ * Kept as a no-op so older callers don't throw — but it cannot identify
252
+ * which subscriber should leave, so it silently does nothing. Any code
253
+ * still calling this will leak listeners and prevent the channel from
254
+ * ever being torn down. Migrate to the subscription handle.
255
+ */
256
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
257
+ leaveConversation(_conversationId) {
258
+ }
259
+ /**
260
+ * Look up the underlying Phoenix Channel for a conversation, if any
261
+ * subscriber is still holding it. All send/push paths go through this
262
+ * helper so the refcounted entry shape is contained to joinConversation.
263
+ */
264
+ getChannel(conversationId) {
265
+ return this.channels.get(conversationId)?.channel ?? null;
215
266
  }
216
267
  /** Send a message to a conversation.
217
268
  *
@@ -223,7 +274,7 @@ var CollabSocket = class {
223
274
  */
224
275
  sendMessage(conversationId, payload) {
225
276
  return new Promise((resolve, reject) => {
226
- const channel = this.channels.get(conversationId);
277
+ const channel = this.getChannel(conversationId);
227
278
  if (!channel) {
228
279
  reject({ code: "NOT_JOINED", message: "Not joined to this conversation" });
229
280
  return;
@@ -246,7 +297,7 @@ var CollabSocket = class {
246
297
  }
247
298
  /** Broadcast typing indicator */
248
299
  sendTyping(conversationId, isTyping) {
249
- const channel = this.channels.get(conversationId);
300
+ const channel = this.getChannel(conversationId);
250
301
  channel?.push(EVENTS.USER_TYPING, {
251
302
  userId: this.config?.userId,
252
303
  userName: this.config?.userName,
@@ -255,7 +306,7 @@ var CollabSocket = class {
255
306
  }
256
307
  /** Mark messages as read */
257
308
  markAsRead(conversationId, messageId) {
258
- const channel = this.channels.get(conversationId);
309
+ const channel = this.getChannel(conversationId);
259
310
  channel?.push(EVENTS.MESSAGE_READ, {
260
311
  messageId,
261
312
  userId: this.config?.userId
@@ -289,7 +340,7 @@ var CollabSocket = class {
289
340
  /** Generic push with promise wrapper */
290
341
  channelPush(conversationId, event, payload) {
291
342
  return new Promise((resolve, reject) => {
292
- const channel = this.channels.get(conversationId);
343
+ const channel = this.getChannel(conversationId);
293
344
  if (!channel) {
294
345
  reject({ code: "NOT_JOINED", message: "Not joined to this conversation" });
295
346
  return;
@@ -306,7 +357,7 @@ var CollabSocket = class {
306
357
  }
307
358
  /** Disconnect socket and leave all channels */
308
359
  disconnect() {
309
- this.channels.forEach((channel) => channel.leave());
360
+ this.channels.forEach((entry) => entry.channel.leave());
310
361
  this.channels.clear();
311
362
  this.presences.clear();
312
363
  this.userChannel?.leave();
@@ -315,6 +366,203 @@ var CollabSocket = class {
315
366
  this.config = null;
316
367
  }
317
368
  };
369
+
370
+ // src/core/theme.ts
371
+ var FONT_SIZE = {
372
+ xs: "12px",
373
+ sm: "13px",
374
+ md: "15px",
375
+ lg: "17px",
376
+ xxl: "28px"
377
+ };
378
+ var FONT_WEIGHT = {
379
+ regular: 400,
380
+ medium: 500,
381
+ semibold: 600,
382
+ bold: 700
383
+ };
384
+ var LINE_HEIGHT = {
385
+ tight: 1.3,
386
+ normal: 1.45};
387
+ var SPACE = {
388
+ S1: "4px",
389
+ S2: "8px",
390
+ S3: "12px",
391
+ S4: "16px",
392
+ S5: "20px",
393
+ S6: "24px",
394
+ S12: "48px"
395
+ };
396
+ var RADIUS = {
397
+ sm: "4px",
398
+ md: "8px",
399
+ lg: "12px",
400
+ xl: "16px",
401
+ pill: "9999px",
402
+ full: "50%"
403
+ };
404
+ var THEME_VAR = {
405
+ primary: "--natoe-colab-primary",
406
+ primaryHover: "--natoe-colab-primary-hover",
407
+ primaryBg: "--natoe-colab-primary-bg",
408
+ primaryFg: "--natoe-colab-primary-fg",
409
+ success: "--natoe-colab-success",
410
+ warning: "--natoe-colab-warning",
411
+ danger: "--natoe-colab-danger",
412
+ dangerBg: "--natoe-colab-danger-bg",
413
+ dangerBorder: "--natoe-colab-danger-border",
414
+ dangerFg: "--natoe-colab-danger-fg",
415
+ fontStack: "--natoe-colab-font-stack",
416
+ // Neutral surface palette — promoted to CSS vars so consumers (e.g.
417
+ // CollabPanel themeMode='dark') can flip the whole grayscale at a subtree
418
+ // level without re-themeing every component.
419
+ white: "--natoe-colab-white",
420
+ neutral50: "--natoe-colab-neutral-50",
421
+ neutral100: "--natoe-colab-neutral-100",
422
+ neutral200: "--natoe-colab-neutral-200",
423
+ neutral300: "--natoe-colab-neutral-300",
424
+ neutral400: "--natoe-colab-neutral-400",
425
+ neutral500: "--natoe-colab-neutral-500",
426
+ neutral600: "--natoe-colab-neutral-600",
427
+ neutral700: "--natoe-colab-neutral-700",
428
+ neutral800: "--natoe-colab-neutral-800",
429
+ neutral900: "--natoe-colab-neutral-900"
430
+ };
431
+ var THEME_DEFAULTS = {
432
+ primary: "#2563eb",
433
+ primaryHover: "#1d4ed8",
434
+ primaryBg: "#dbeafe",
435
+ primaryFg: "#1d4ed8",
436
+ success: "#059669",
437
+ warning: "#d97706",
438
+ danger: "#dc2626",
439
+ dangerBg: "#fef2f2",
440
+ dangerBorder: "#fecaca",
441
+ dangerFg: "#b91c1c",
442
+ fontStack: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
443
+ // Neutral grayscale — these defaults are the same hex literals the
444
+ // package shipped with; they're now overridable per-subtree.
445
+ white: "#ffffff",
446
+ neutral50: "#f9fafb",
447
+ neutral100: "#f3f4f6",
448
+ neutral200: "#e5e7eb",
449
+ neutral300: "#d1d5db",
450
+ neutral400: "#9ca3af",
451
+ neutral500: "#6b7280",
452
+ neutral600: "#4b5563",
453
+ neutral700: "#374151",
454
+ neutral800: "#1f2937",
455
+ neutral900: "#111827"
456
+ };
457
+ var cssVar = (name, fallback) => `var(${name}, ${fallback})`;
458
+ var COLOR = {
459
+ // Brand — themeable
460
+ primary: cssVar(THEME_VAR.primary, THEME_DEFAULTS.primary),
461
+ primaryHover: cssVar(THEME_VAR.primaryHover, THEME_DEFAULTS.primaryHover),
462
+ /** Tinted surface for chips/cards on brand-coloured states. */
463
+ primaryBg: cssVar(THEME_VAR.primaryBg, THEME_DEFAULTS.primaryBg),
464
+ primaryFg: cssVar(THEME_VAR.primaryFg, THEME_DEFAULTS.primaryFg),
465
+ // Neutral grayscale — themeable via CSS variables. Light-mode defaults
466
+ // taken from Tailwind's zinc-leaning slate to match the package's
467
+ // existing tonal balance; a host can override the entire palette by
468
+ // setting --natoe-colab-* values on any ancestor element (used by
469
+ // CollabPanel themeMode='dark' for the viewer's left-panel surface).
470
+ white: cssVar(THEME_VAR.white, THEME_DEFAULTS.white),
471
+ neutral50: cssVar(THEME_VAR.neutral50, THEME_DEFAULTS.neutral50),
472
+ neutral100: cssVar(THEME_VAR.neutral100, THEME_DEFAULTS.neutral100),
473
+ neutral200: cssVar(THEME_VAR.neutral200, THEME_DEFAULTS.neutral200),
474
+ neutral300: cssVar(THEME_VAR.neutral300, THEME_DEFAULTS.neutral300),
475
+ neutral400: cssVar(THEME_VAR.neutral400, THEME_DEFAULTS.neutral400),
476
+ neutral500: cssVar(THEME_VAR.neutral500, THEME_DEFAULTS.neutral500),
477
+ neutral600: cssVar(THEME_VAR.neutral600, THEME_DEFAULTS.neutral600),
478
+ neutral700: cssVar(THEME_VAR.neutral700, THEME_DEFAULTS.neutral700),
479
+ neutral800: cssVar(THEME_VAR.neutral800, THEME_DEFAULTS.neutral800),
480
+ neutral900: cssVar(THEME_VAR.neutral900, THEME_DEFAULTS.neutral900),
481
+ slate800: "#1e293b",
482
+ // Semantic — themeable
483
+ success: cssVar(THEME_VAR.success, THEME_DEFAULTS.success),
484
+ warning: cssVar(THEME_VAR.warning, THEME_DEFAULTS.warning),
485
+ danger: cssVar(THEME_VAR.danger, THEME_DEFAULTS.danger),
486
+ dangerBg: cssVar(THEME_VAR.dangerBg, THEME_DEFAULTS.dangerBg),
487
+ dangerBorder: cssVar(THEME_VAR.dangerBorder, THEME_DEFAULTS.dangerBorder),
488
+ dangerFg: cssVar(THEME_VAR.dangerFg, THEME_DEFAULTS.dangerFg)
489
+ };
490
+ var SIZE = {
491
+ /** Default interactive height (text buttons, list rows). */
492
+ control: "40px",
493
+ /** Square icon-only buttons inside chrome (popup title, menu trigger). */
494
+ controlIcon: "36px"};
495
+ var SHADOW = {
496
+ /** Cards / floating popovers (message-actions menu). */
497
+ popover: "0 6px 18px rgba(0, 0, 0, 0.12)",
498
+ toast: "0 4px 12px rgba(0, 0, 0, 0.18)"
499
+ };
500
+ var Z_INDEX = {
501
+ /** Sticky day divider — above bubbles, below interactive popovers. */
502
+ sticky: 1,
503
+ /** Toasts inside a panel. */
504
+ toast: 20,
505
+ /** Floating CollabPopup dialog. */
506
+ dialog: 9999,
507
+ /** Portaled message-actions menu — must sit above the dialog. */
508
+ menu: 1e4
509
+ };
510
+
511
+ // src/core/styles.ts
512
+ var FONT_STACK = `var(${THEME_VAR.fontStack}, ${THEME_DEFAULTS.fontStack})`;
513
+ var ROOT_CLASS = "natoe-colab-root";
514
+ var GLOBAL_STYLE_ID = "natoe-colab-global-styles";
515
+ var ROOT_DEFAULTS_BLOCK = Object.keys(THEME_DEFAULTS).map((key) => ` ${THEME_VAR[key]}: ${THEME_DEFAULTS[key]};`).join("\n");
516
+ var GLOBAL_CSS = `
517
+ :root {
518
+ ${ROOT_DEFAULTS_BLOCK}
519
+ }
520
+
521
+ @keyframes natoe-colab-spin { to { transform: rotate(360deg); } }
522
+
523
+ @keyframes natoe-colab-message-in {
524
+ from { opacity: 0; transform: translateY(4px); }
525
+ to { opacity: 1; transform: translateY(0); }
526
+ }
527
+
528
+ /* High-contrast adjustments \u2014 older users on OS-level high-contrast mode
529
+ get heavier borders and stronger text without losing the package look. */
530
+ @media (prefers-contrast: more) {
531
+ .${ROOT_CLASS} {
532
+ color: #000000;
533
+ }
534
+ .${ROOT_CLASS} button {
535
+ outline: 1px solid currentColor;
536
+ }
537
+ }
538
+
539
+ /* Honour reduced-motion preferences by killing entry animations. */
540
+ @media (prefers-reduced-motion: reduce) {
541
+ .${ROOT_CLASS} [data-natoe-message-bubble] {
542
+ animation: none !important;
543
+ }
544
+ }
545
+ `;
546
+ function ensureGlobalStyles() {
547
+ if (typeof document === "undefined") return;
548
+ if (document.getElementById(GLOBAL_STYLE_ID)) return;
549
+ const style = document.createElement("style");
550
+ style.id = GLOBAL_STYLE_ID;
551
+ style.textContent = GLOBAL_CSS;
552
+ document.head.appendChild(style);
553
+ }
554
+ function applyThemeOverrides(theme) {
555
+ if (typeof document === "undefined") return;
556
+ const root = document.documentElement;
557
+ Object.keys(THEME_VAR).forEach((key) => {
558
+ const value = theme?.[key];
559
+ if (value) {
560
+ root.style.setProperty(THEME_VAR[key], value);
561
+ } else {
562
+ root.style.removeProperty(THEME_VAR[key]);
563
+ }
564
+ });
565
+ }
318
566
  var CollabContext = createContext(null);
319
567
  function useCollab() {
320
568
  const context = useContext(CollabContext);
@@ -333,6 +581,9 @@ function CollabProvider({ config, apiBaseUrl, children }) {
333
581
  const pendingResolvers = useRef(/* @__PURE__ */ new Map());
334
582
  const previewCache = useRef(/* @__PURE__ */ new Map());
335
583
  const batchScheduled = useRef(false);
584
+ useEffect(() => {
585
+ applyThemeOverrides(config.theme);
586
+ }, [config.theme]);
336
587
  useEffect(() => {
337
588
  if (typeof window === "undefined") return;
338
589
  let s = socket;
@@ -548,6 +799,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
548
799
  config,
549
800
  apiBaseUrl,
550
801
  totalUnread,
802
+ unreadCounts,
551
803
  requestPreview,
552
804
  invalidatePreview,
553
805
  fetchMessages,
@@ -604,6 +856,7 @@ function useConversation({
604
856
  const [isConnected, setIsConnected] = useState(false);
605
857
  const [replyTo, setReplyTo] = useState(null);
606
858
  const joinedConversationId = useRef(null);
859
+ const channelSubscription = useRef(null);
607
860
  const typingTimers = useRef(/* @__PURE__ */ new Map());
608
861
  const ensureConversationInFlight = useRef(null);
609
862
  const markedReadIdsRef = useRef(/* @__PURE__ */ new Set());
@@ -617,7 +870,9 @@ function useConversation({
617
870
  const joinChannel = useCallback(
618
871
  (conv) => {
619
872
  if (joinedConversationId.current === conv.id) return;
620
- socket.joinConversation(conv.id, {
873
+ channelSubscription.current?.release();
874
+ channelSubscription.current = null;
875
+ const subscription = socket.joinConversation(conv.id, {
621
876
  // Dedupe by id so an optimistic message (added client-side on send)
622
877
  // doesn't double up when the server's broadcast arrives.
623
878
  onMessage: (msg) => setMessages((prev) => {
@@ -667,6 +922,8 @@ function useConversation({
667
922
  onChannelDeleted: () => {
668
923
  setConversation(null);
669
924
  setMessages([]);
925
+ channelSubscription.current?.release();
926
+ channelSubscription.current = null;
670
927
  joinedConversationId.current = null;
671
928
  setIsConnected(false);
672
929
  },
@@ -692,6 +949,7 @@ function useConversation({
692
949
  );
693
950
  }
694
951
  });
952
+ channelSubscription.current = subscription;
695
953
  joinedConversationId.current = conv.id;
696
954
  setIsConnected(true);
697
955
  },
@@ -725,26 +983,22 @@ function useConversation({
725
983
  setConversation(existingConv);
726
984
  setParticipants(preview.participants);
727
985
  joinChannel(existingConv);
728
- setMessages(preview.lastMessages);
729
- setHasMore(preview.messageCount > preview.lastMessages.length);
730
- setPinnedMessages(preview.lastMessages.filter((m) => m.isPinned));
731
- setIsLoading(false);
732
986
  if (loadHistory) {
733
987
  fetchMessages(preview.conversationId).then((history) => {
734
988
  if (cancelled) return;
735
- setMessages((prev) => {
736
- const seen = new Set(prev.map((m) => m.id));
737
- const older = history.filter((m) => !seen.has(m.id));
738
- return [...older, ...prev];
739
- });
740
- setHasMore(history.length > 0);
741
- setPinnedMessages((prev) => {
742
- const fromHistory = history.filter((m) => m.isPinned);
743
- const seen = new Set(prev.map((m) => m.id));
744
- return [...fromHistory.filter((m) => !seen.has(m.id)), ...prev];
745
- });
989
+ setMessages(history);
990
+ setHasMore(history.length >= MESSAGES_PAGE_SIZE);
991
+ setPinnedMessages(history.filter((m) => m.isPinned));
992
+ setIsLoading(false);
746
993
  }).catch(() => {
994
+ if (cancelled) return;
995
+ setIsLoading(false);
747
996
  });
997
+ } else {
998
+ setMessages([]);
999
+ setHasMore(false);
1000
+ setPinnedMessages([]);
1001
+ setIsLoading(false);
748
1002
  }
749
1003
  } catch (err) {
750
1004
  if (cancelled) return;
@@ -756,10 +1010,9 @@ function useConversation({
756
1010
  init();
757
1011
  return () => {
758
1012
  cancelled = true;
759
- if (joinedConversationId.current) {
760
- socket.leaveConversation(joinedConversationId.current);
761
- joinedConversationId.current = null;
762
- }
1013
+ channelSubscription.current?.release();
1014
+ channelSubscription.current = null;
1015
+ joinedConversationId.current = null;
763
1016
  typingTimers.current.forEach((timer) => clearTimeout(timer));
764
1017
  typingTimers.current.clear();
765
1018
  markedReadIdsRef.current.clear();
@@ -825,7 +1078,7 @@ function useConversation({
825
1078
  const payload = {
826
1079
  body,
827
1080
  type: "text",
828
- ...replyTo ? { replyToId: replyTo.id } : {}
1081
+ ...replyTo ? { reply_to_id: replyTo.id } : {}
829
1082
  };
830
1083
  const { conv, persistedByCreate } = await ensureConversation(payload);
831
1084
  setReplyTo(null);
@@ -878,7 +1131,7 @@ function useConversation({
878
1131
  const payload = {
879
1132
  body: target.body,
880
1133
  type: "text",
881
- ...target.replyToId ? { replyToId: target.replyToId } : {}
1134
+ ...target.replyToId ? { reply_to_id: target.replyToId } : {}
882
1135
  };
883
1136
  try {
884
1137
  await socket.sendMessage(conversation.id, payload);
@@ -1175,17 +1428,26 @@ function DicomIcon({ size = 18, color = "currentColor" }) {
1175
1428
  }
1176
1429
  );
1177
1430
  }
1178
- function SettingsIcon({ size = 18, color = "currentColor" }) {
1179
- return /* @__PURE__ */ jsx(
1431
+ function PeopleIcon({ size = 18, color = "currentColor" }) {
1432
+ return /* @__PURE__ */ jsxs(
1180
1433
  "svg",
1181
1434
  {
1182
1435
  xmlns: "http://www.w3.org/2000/svg",
1183
1436
  width: size,
1184
1437
  height: size,
1185
1438
  viewBox: "0 0 24 24",
1186
- fill: color,
1439
+ fill: "none",
1440
+ stroke: color,
1441
+ strokeWidth: "2",
1442
+ strokeLinecap: "round",
1443
+ strokeLinejoin: "round",
1187
1444
  "aria-hidden": "true",
1188
- children: /* @__PURE__ */ jsx("path", { d: "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" })
1445
+ children: [
1446
+ /* @__PURE__ */ jsx("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
1447
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "7", r: "4" }),
1448
+ /* @__PURE__ */ jsx("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }),
1449
+ /* @__PURE__ */ jsx("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })
1450
+ ]
1189
1451
  }
1190
1452
  );
1191
1453
  }
@@ -1203,102 +1465,6 @@ function BackIcon({ size = 22, color = "currentColor" }) {
1203
1465
  }
1204
1466
  );
1205
1467
  }
1206
-
1207
- // src/core/theme.ts
1208
- var FONT_SIZE = {
1209
- xs: "12px",
1210
- sm: "13px",
1211
- md: "15px",
1212
- lg: "17px",
1213
- xxl: "28px"
1214
- };
1215
- var FONT_WEIGHT = {
1216
- regular: 400,
1217
- medium: 500,
1218
- semibold: 600,
1219
- bold: 700
1220
- };
1221
- var LINE_HEIGHT = {
1222
- tight: 1.3,
1223
- normal: 1.45};
1224
- var SPACE = {
1225
- S1: "4px",
1226
- S2: "8px",
1227
- S3: "12px",
1228
- S4: "16px",
1229
- S5: "20px",
1230
- S6: "24px",
1231
- S12: "48px"
1232
- };
1233
- var RADIUS = {
1234
- sm: "4px",
1235
- md: "8px",
1236
- lg: "12px",
1237
- pill: "9999px",
1238
- full: "50%"
1239
- };
1240
- var COLOR = {
1241
- // Brand
1242
- primary: "#2563eb",
1243
- /** Tinted surface for chips/cards on brand-coloured states. */
1244
- primaryBg: "#dbeafe",
1245
- primaryFg: "#1d4ed8",
1246
- // Neutral grayscale — taken from Tailwind's zinc-leaning slate, the
1247
- // existing dominant family in the package.
1248
- white: "#ffffff",
1249
- neutral50: "#f9fafb",
1250
- neutral100: "#f3f4f6",
1251
- neutral200: "#e5e7eb",
1252
- neutral300: "#d1d5db",
1253
- neutral400: "#9ca3af",
1254
- neutral500: "#6b7280",
1255
- neutral600: "#4b5563",
1256
- neutral700: "#374151",
1257
- neutral800: "#1f2937",
1258
- neutral900: "#111827",
1259
- // Slate (used by day dividers + dialog title bar)
1260
- slate200: "#e2e8f0",
1261
- slate700: "#334155",
1262
- slate800: "#1e293b",
1263
- // Semantic
1264
- success: "#059669",
1265
- warning: "#d97706",
1266
- danger: "#dc2626",
1267
- dangerBg: "#fef2f2",
1268
- dangerBorder: "#fecaca",
1269
- dangerFg: "#b91c1c"
1270
- };
1271
- var SIZE = {
1272
- /** Default interactive height (text buttons, list rows). */
1273
- control: "40px",
1274
- /** Square icon-only buttons inside chrome (popup title, menu trigger). */
1275
- controlIcon: "36px",
1276
- /** Primary input controls (textarea send/attach). 44px = WCAG min. */
1277
- controlPrimary: "44px",
1278
- /** Avatar in the inbox row. */
1279
- avatar: "40px",
1280
- /** Unread dot. */
1281
- dot: "10px"
1282
- };
1283
- var SHADOW = {
1284
- /** Cards / floating popovers (message-actions menu). */
1285
- popover: "0 6px 18px rgba(0, 0, 0, 0.12)",
1286
- /** Dialog (CollabPopup). */
1287
- dialog: "0 8px 30px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08)",
1288
- /** Subtle separator shadow (sticky day divider, toast). */
1289
- subtle: "0 1px 2px rgba(0, 0, 0, 0.04)",
1290
- toast: "0 4px 12px rgba(0, 0, 0, 0.18)"
1291
- };
1292
- var Z_INDEX = {
1293
- /** Sticky day divider — above bubbles, below interactive popovers. */
1294
- sticky: 1,
1295
- /** Toasts inside a panel. */
1296
- toast: 20,
1297
- /** Floating CollabPopup dialog. */
1298
- dialog: 9999,
1299
- /** Portaled message-actions menu — must sit above the dialog. */
1300
- menu: 1e4
1301
- };
1302
1468
  function resolveDisplayName(patientData, displayName) {
1303
1469
  const localComplete = !!patientData.labName && !!patientData.displayOrderId;
1304
1470
  if (localComplete) return buildChannelName(patientData);
@@ -1306,6 +1472,7 @@ function resolveDisplayName(patientData, displayName) {
1306
1472
  }
1307
1473
  function PatientHeader({
1308
1474
  patientData,
1475
+ participants,
1309
1476
  onOpenDicom,
1310
1477
  onOpenSettings,
1311
1478
  onBack,
@@ -1315,15 +1482,28 @@ function PatientHeader({
1315
1482
  }) {
1316
1483
  const hasDicom = !!(patientData.studyId && patientData.storageId);
1317
1484
  const meta = [];
1318
- if (patientData.patientAge) meta.push({ label: "Age", value: String(patientData.patientAge) });
1319
- if (patientData.patientSex) meta.push({ label: "Sex", value: String(patientData.patientSex) });
1320
- if (patientData.studyType) meta.push({ label: "Modality", value: patientData.studyType });
1321
- if (patientData.bodyParts && patientData.bodyParts.length > 0) {
1322
- meta.push({ label: "Body part", value: patientData.bodyParts.join(", ") });
1485
+ if (patientData.patientAge && patientData.patientSex) {
1486
+ meta.push({
1487
+ key: "ageSex",
1488
+ label: "Age / Sex",
1489
+ value: `${patientData.patientAge} \xB7 ${patientData.patientSex}`
1490
+ });
1491
+ } else if (patientData.patientAge) {
1492
+ meta.push({ key: "age", label: "Age", value: String(patientData.patientAge) });
1493
+ } else if (patientData.patientSex) {
1494
+ meta.push({ key: "sex", label: "Sex", value: String(patientData.patientSex) });
1495
+ }
1496
+ if (patientData.studyType) {
1497
+ meta.push({ key: "modality", label: "Modality", value: patientData.studyType });
1323
1498
  }
1324
- if (patientData.referringPhysician) {
1325
- meta.push({ label: "Ref", value: patientData.referringPhysician });
1499
+ if (patientData.bodyParts && patientData.bodyParts.length > 0) {
1500
+ meta.push({
1501
+ key: "body",
1502
+ label: "Body part",
1503
+ value: patientData.bodyParts.join(", ")
1504
+ });
1326
1505
  }
1506
+ const hasActions = hasDicom && onOpenDicom || onOpenSettings;
1327
1507
  return /* @__PURE__ */ jsxs("div", { className, style: styles.container, children: [
1328
1508
  !hideName && /* @__PURE__ */ jsxs("div", { style: styles.nameRow, children: [
1329
1509
  onBack && /* @__PURE__ */ jsx(
@@ -1339,54 +1519,58 @@ function PatientHeader({
1339
1519
  ),
1340
1520
  /* @__PURE__ */ jsx("span", { style: styles.nameText, children: resolveDisplayName(patientData, displayName) })
1341
1521
  ] }),
1342
- meta.length > 0 && /* @__PURE__ */ jsx("div", { style: styles.metaRow, children: meta.map((item) => /* @__PURE__ */ jsxs("span", { style: styles.metaItem, children: [
1343
- /* @__PURE__ */ jsxs("span", { style: styles.metaLabel, children: [
1344
- item.label,
1345
- ":"
1346
- ] }),
1347
- " ",
1348
- /* @__PURE__ */ jsx("span", { style: styles.metaValue, children: item.value })
1349
- ] }, item.label)) }),
1350
- hasDicom && onOpenDicom || onOpenSettings ? /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
1351
- hasDicom && onOpenDicom && /* @__PURE__ */ jsxs(
1352
- "button",
1353
- {
1354
- onClick: onOpenDicom,
1355
- style: styles.dicomButton,
1356
- type: "button",
1357
- "aria-label": "View DICOM study",
1358
- children: [
1359
- /* @__PURE__ */ jsx(DicomIcon, { size: 18, color: COLOR.white }),
1360
- /* @__PURE__ */ jsx("span", { children: "View DICOM" })
1361
- ]
1362
- }
1363
- ),
1364
- onOpenSettings && /* @__PURE__ */ jsxs(
1365
- "button",
1366
- {
1367
- onClick: onOpenSettings,
1368
- style: styles.settingsButton,
1369
- type: "button",
1370
- "aria-label": "Open channel settings",
1371
- children: [
1372
- /* @__PURE__ */ jsx(SettingsIcon, { size: 18, color: COLOR.neutral700 }),
1373
- /* @__PURE__ */ jsx("span", { children: "Settings" })
1374
- ]
1375
- }
1376
- )
1377
- ] }) : null
1522
+ /* @__PURE__ */ jsxs("div", { style: styles.caseCard, children: [
1523
+ meta.length > 0 && /* @__PURE__ */ jsx("div", { style: styles.metaRow, children: meta.map((item) => /* @__PURE__ */ jsxs("div", { style: styles.metaCol, children: [
1524
+ /* @__PURE__ */ jsx("span", { style: styles.metaLabel, children: item.label }),
1525
+ /* @__PURE__ */ jsx("span", { style: styles.metaValue, children: item.value })
1526
+ ] }, item.key)) }),
1527
+ hasActions && /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
1528
+ hasDicom && onOpenDicom && /* @__PURE__ */ jsxs(
1529
+ "button",
1530
+ {
1531
+ onClick: onOpenDicom,
1532
+ style: styles.dicomButton,
1533
+ type: "button",
1534
+ "aria-label": "View DICOM study",
1535
+ children: [
1536
+ /* @__PURE__ */ jsx(DicomIcon, { size: 18, color: COLOR.primary }),
1537
+ /* @__PURE__ */ jsx("span", { children: "View DICOM" })
1538
+ ]
1539
+ }
1540
+ ),
1541
+ onOpenSettings && /* @__PURE__ */ jsxs(
1542
+ "button",
1543
+ {
1544
+ onClick: onOpenSettings,
1545
+ style: styles.settingsIconButton,
1546
+ type: "button",
1547
+ "aria-label": `Open channel settings (${participants.length} participants)`,
1548
+ title: "Channel participants",
1549
+ children: [
1550
+ /* @__PURE__ */ jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
1551
+ /* @__PURE__ */ jsx("span", { style: styles.participantCount, children: participants.length })
1552
+ ]
1553
+ }
1554
+ )
1555
+ ] })
1556
+ ] })
1378
1557
  ] });
1379
1558
  }
1380
1559
  var styles = {
1381
1560
  container: {
1382
- padding: `${SPACE.S3} ${SPACE.S4}`,
1383
- borderBottom: `1px solid ${COLOR.neutral200}`,
1384
- backgroundColor: COLOR.neutral50
1561
+ display: "flex",
1562
+ flexDirection: "column",
1563
+ backgroundColor: COLOR.primaryBg,
1564
+ borderBottom: `1px solid ${COLOR.neutral200}`
1385
1565
  },
1566
+ // Optional name row, only when `hideName` is false (compact-inbox path)
1386
1567
  nameRow: {
1387
1568
  display: "flex",
1388
1569
  alignItems: "center",
1389
- gap: SPACE.S2
1570
+ gap: SPACE.S2,
1571
+ padding: `${SPACE.S3} ${SPACE.S4} ${SPACE.S2}`,
1572
+ borderBottom: `1px solid ${COLOR.neutral200}`,
1573
+ backgroundColor: COLOR.white
1390
1574
  },
1391
1575
  backButton: {
1392
1576
  width: SIZE.controlIcon,
@@ -1403,65 +1587,98 @@ var styles = {
1403
1587
  },
1404
1588
  nameText: {
1405
1589
  fontSize: FONT_SIZE.lg,
1406
- fontWeight: FONT_WEIGHT.semibold,
1590
+ fontWeight: FONT_WEIGHT.bold,
1407
1591
  color: COLOR.neutral900,
1408
1592
  lineHeight: LINE_HEIGHT.tight,
1593
+ letterSpacing: "-0.01em",
1409
1594
  minWidth: 0,
1410
1595
  overflow: "hidden",
1411
1596
  textOverflow: "ellipsis",
1412
1597
  whiteSpace: "nowrap"
1413
1598
  },
1599
+ // Case card: meta columns flowing in a wrap-row, actions stacked
1600
+ // below. Each meta column is a small label-above-value pair so the
1601
+ // user can scan field names quickly without a legend.
1602
+ caseCard: {
1603
+ display: "flex",
1604
+ flexDirection: "column",
1605
+ gap: SPACE.S3,
1606
+ padding: `${SPACE.S3} ${SPACE.S5}`
1607
+ },
1608
+ // Wrap-row of stacked label/value columns. Compact column gap keeps
1609
+ // the row dense without crowding; row gap kicks in when columns
1610
+ // wrap to a second line on narrow popups.
1414
1611
  metaRow: {
1415
1612
  display: "flex",
1416
1613
  flexWrap: "wrap",
1417
- columnGap: SPACE.S3,
1418
- rowGap: SPACE.S1,
1419
- marginTop: SPACE.S1
1614
+ columnGap: SPACE.S5,
1615
+ rowGap: SPACE.S2,
1616
+ minWidth: 0
1420
1617
  },
1421
- metaItem: {
1422
- fontSize: FONT_SIZE.sm,
1423
- color: COLOR.neutral700,
1424
- whiteSpace: "nowrap"
1618
+ metaCol: {
1619
+ display: "flex",
1620
+ flexDirection: "column",
1621
+ gap: "2px",
1622
+ minWidth: 0
1425
1623
  },
1426
1624
  metaLabel: {
1625
+ fontSize: "11px",
1626
+ fontWeight: FONT_WEIGHT.bold,
1627
+ letterSpacing: "0.06em",
1628
+ textTransform: "uppercase",
1427
1629
  color: COLOR.neutral500,
1428
- fontWeight: FONT_WEIGHT.medium
1630
+ whiteSpace: "nowrap"
1429
1631
  },
1430
1632
  metaValue: {
1431
- color: COLOR.neutral900
1633
+ fontSize: FONT_SIZE.sm,
1634
+ fontWeight: FONT_WEIGHT.semibold,
1635
+ color: COLOR.neutral900,
1636
+ whiteSpace: "nowrap",
1637
+ overflow: "hidden",
1638
+ textOverflow: "ellipsis",
1639
+ maxWidth: "180px"
1432
1640
  },
1433
1641
  actions: {
1434
1642
  display: "flex",
1435
1643
  gap: SPACE.S2,
1436
- marginTop: SPACE.S3
1644
+ flexShrink: 0
1437
1645
  },
1438
1646
  dicomButton: {
1439
1647
  display: "inline-flex",
1440
1648
  alignItems: "center",
1441
1649
  gap: SPACE.S2,
1442
1650
  minHeight: SIZE.control,
1443
- padding: `${SPACE.S2} ${SPACE.S4}`,
1651
+ padding: `${SPACE.S2} 14px`,
1444
1652
  fontSize: FONT_SIZE.sm,
1445
- fontWeight: FONT_WEIGHT.medium,
1446
- color: COLOR.white,
1447
- backgroundColor: COLOR.primary,
1448
- border: "none",
1449
- borderRadius: RADIUS.md,
1653
+ fontWeight: FONT_WEIGHT.semibold,
1654
+ color: COLOR.primary,
1655
+ backgroundColor: COLOR.white,
1656
+ border: `1.5px solid ${COLOR.primary}`,
1657
+ borderRadius: RADIUS.lg,
1450
1658
  cursor: "pointer"
1451
1659
  },
1452
- settingsButton: {
1660
+ // Channel-info button: people icon + participant count. Click opens the
1661
+ // channel settings overlay (kept on the same handler as before so hosts
1662
+ // don't have to re-wire — the affordance just looks like a "members"
1663
+ // pill now instead of a gear).
1664
+ settingsIconButton: {
1665
+ minHeight: SIZE.control,
1453
1666
  display: "inline-flex",
1454
1667
  alignItems: "center",
1455
1668
  gap: SPACE.S2,
1456
- minHeight: SIZE.control,
1457
- padding: `${SPACE.S2} ${SPACE.S4}`,
1669
+ padding: `0 ${SPACE.S3}`,
1670
+ backgroundColor: COLOR.white,
1671
+ border: `1px solid ${COLOR.neutral200}`,
1672
+ borderRadius: RADIUS.lg,
1673
+ color: COLOR.neutral600,
1674
+ cursor: "pointer",
1675
+ flexShrink: 0
1676
+ },
1677
+ participantCount: {
1458
1678
  fontSize: FONT_SIZE.sm,
1459
- fontWeight: FONT_WEIGHT.medium,
1679
+ fontWeight: FONT_WEIGHT.semibold,
1460
1680
  color: COLOR.neutral700,
1461
- backgroundColor: COLOR.white,
1462
- border: `1px solid ${COLOR.neutral300}`,
1463
- borderRadius: RADIUS.md,
1464
- cursor: "pointer"
1681
+ fontVariantNumeric: "tabular-nums"
1465
1682
  }
1466
1683
  };
1467
1684
  function ReplyQuoteBlock({
@@ -1470,10 +1687,10 @@ function ReplyQuoteBlock({
1470
1687
  onClick,
1471
1688
  className
1472
1689
  }) {
1473
- const baseBg = inOwnBubble ? "rgba(255,255,255,0.18)" : COLOR.neutral100;
1690
+ const baseBg = inOwnBubble ? "rgba(255,255,255,0.22)" : COLOR.white;
1474
1691
  const barColor = inOwnBubble ? COLOR.white : COLOR.primary;
1475
1692
  const nameColor = inOwnBubble ? COLOR.white : COLOR.primary;
1476
- const bodyColor = inOwnBubble ? "rgba(255,255,255,0.85)" : COLOR.neutral600;
1693
+ const bodyColor = inOwnBubble ? "rgba(255,255,255,0.9)" : COLOR.neutral700;
1477
1694
  const preview = renderSnapshotPreview(snapshot);
1478
1695
  return /* @__PURE__ */ jsxs(
1479
1696
  "div",
@@ -1845,43 +2062,50 @@ var styles4 = {
1845
2062
  position: "relative",
1846
2063
  display: "inline-block"
1847
2064
  },
2065
+ // Pill-shaped trigger matching the prototype's `.nc-msg-act` style —
2066
+ // always visible (touch-friendly) rather than hover-only.
1848
2067
  trigger: {
1849
- width: SIZE.controlIcon,
1850
- height: SIZE.controlIcon,
1851
- display: "flex",
2068
+ display: "inline-flex",
1852
2069
  alignItems: "center",
1853
2070
  justifyContent: "center",
1854
- backgroundColor: "transparent",
1855
- border: "none",
1856
- borderRadius: RADIUS.full,
2071
+ width: "32px",
2072
+ height: "32px",
2073
+ backgroundColor: COLOR.white,
2074
+ border: `1px solid ${COLOR.neutral200}`,
2075
+ borderRadius: "999px",
1857
2076
  cursor: "pointer",
1858
- color: COLOR.neutral600
2077
+ color: COLOR.neutral600,
2078
+ transition: "background-color 120ms ease, color 120ms ease, border-color 120ms ease"
1859
2079
  },
1860
2080
  menu: {
1861
2081
  position: "fixed",
1862
- minWidth: "160px",
2082
+ minWidth: "200px",
1863
2083
  backgroundColor: COLOR.white,
1864
- border: `1px solid ${COLOR.neutral200}`,
1865
- borderRadius: RADIUS.md,
2084
+ border: `1px solid ${COLOR.neutral300}`,
2085
+ borderRadius: RADIUS.lg,
1866
2086
  boxShadow: SHADOW.popover,
1867
- padding: SPACE.S1,
2087
+ padding: "6px",
2088
+ display: "flex",
2089
+ flexDirection: "column",
2090
+ gap: "2px",
1868
2091
  // Above the floating popup (z-index dialog) but below any future modal.
1869
2092
  zIndex: Z_INDEX.menu
1870
2093
  },
1871
2094
  item: {
1872
2095
  display: "flex",
1873
2096
  alignItems: "center",
1874
- gap: SPACE.S3,
2097
+ gap: SPACE.S2,
1875
2098
  width: "100%",
1876
2099
  minHeight: SIZE.control,
1877
- padding: `${SPACE.S2} ${SPACE.S3}`,
2100
+ padding: `10px ${SPACE.S3}`,
1878
2101
  fontSize: FONT_SIZE.sm,
1879
- color: COLOR.neutral900,
2102
+ color: COLOR.neutral700,
1880
2103
  backgroundColor: "transparent",
1881
2104
  border: "none",
1882
- borderRadius: RADIUS.sm,
2105
+ borderRadius: RADIUS.md,
1883
2106
  cursor: "pointer",
1884
- textAlign: "left"
2107
+ textAlign: "left",
2108
+ fontFamily: "inherit"
1885
2109
  },
1886
2110
  itemDisabled: {
1887
2111
  color: COLOR.neutral400,
@@ -1995,71 +2219,116 @@ function MessageBubble({
1995
2219
  },
1996
2220
  children: [
1997
2221
  isOwn && actionsSlot,
1998
- /* @__PURE__ */ jsx("div", { style: styles5.bubbleWrapper, children: /* @__PURE__ */ jsxs(
1999
- "div",
2000
- {
2001
- style: {
2002
- ...styles5.bubble,
2003
- ...isOwn ? styles5.ownBubble : styles5.otherBubble
2004
- },
2005
- children: [
2006
- message.isPinned && /* @__PURE__ */ jsxs("div", { style: styles5.pinnedBadge, children: [
2007
- /* @__PURE__ */ jsx(PinIcon, { size: 12 }),
2008
- /* @__PURE__ */ jsx("span", { children: "Pinned" })
2009
- ] }),
2010
- message.replyToSnapshot && /* @__PURE__ */ jsx(
2011
- ReplyQuoteBlock,
2012
- {
2013
- snapshot: message.replyToSnapshot,
2014
- inOwnBubble: isOwn,
2015
- onClick: onReplyJumpTo
2016
- }
2017
- ),
2018
- !isOwn && /* @__PURE__ */ jsxs("div", { style: styles5.senderRow, children: [
2019
- /* @__PURE__ */ jsx("span", { style: styles5.senderName, children: message.senderName }),
2020
- /* @__PURE__ */ jsx(RoleBadge, { role: message.senderRole })
2021
- ] }),
2022
- message.type === "text" && /* @__PURE__ */ jsx(TextContent, { body: message.body, onDeepLinkClick }),
2023
- message.type === "audio" && /* @__PURE__ */ jsx(AudioContent, { mediaUrl: message.mediaUrl, duration: message.mediaDuration }),
2024
- message.type === "image" && /* @__PURE__ */ jsx(ImageContent, { mediaUrl: message.mediaUrl, fileName: message.fileName }),
2025
- message.type === "file" && /* @__PURE__ */ jsx(FileContent, { mediaUrl: message.mediaUrl, fileName: message.fileName }),
2026
- message.type === "deep_link" && /* @__PURE__ */ jsx(DeepLinkContent, { body: message.body, metadata: message.metadata, onDeepLinkClick }),
2027
- /* @__PURE__ */ jsx("div", { style: styles5.timestamp, children: formatTime(message.insertedAt) }),
2028
- isOwn && message.status === "sending" && /* @__PURE__ */ jsxs("div", { style: styles5.statusRow, children: [
2029
- /* @__PURE__ */ jsx(Spinner, { size: 12, color: "rgba(255,255,255,0.85)" }),
2030
- /* @__PURE__ */ jsx("span", { style: styles5.statusText, children: "Sending\u2026" })
2031
- ] }),
2032
- isOwn && message.status === "failed" && /* @__PURE__ */ jsxs("div", { style: styles5.statusRow, children: [
2033
- /* @__PURE__ */ jsx(AlertIcon, { size: 12, color: "#fecaca" }),
2034
- /* @__PURE__ */ jsx("span", { style: styles5.statusText, children: "Not sent" }),
2035
- onRetry && /* @__PURE__ */ jsx(
2036
- "button",
2222
+ /* @__PURE__ */ jsxs("div", { style: styles5.bubbleWrapper, children: [
2223
+ !isOwn && /* @__PURE__ */ jsxs("div", { style: styles5.authorRow, children: [
2224
+ /* @__PURE__ */ jsx(
2225
+ "span",
2226
+ {
2227
+ style: {
2228
+ ...styles5.authorAvatar,
2229
+ backgroundColor: roleColor(message.senderRole)
2230
+ },
2231
+ "aria-hidden": "true",
2232
+ children: computeInitials(message.senderName)
2233
+ }
2234
+ ),
2235
+ /* @__PURE__ */ jsx("span", { style: styles5.authorName, children: message.senderName }),
2236
+ /* @__PURE__ */ jsx(RoleBadge, { role: message.senderRole })
2237
+ ] }),
2238
+ /* @__PURE__ */ jsxs(
2239
+ "div",
2240
+ {
2241
+ style: {
2242
+ ...styles5.bubble,
2243
+ ...isOwn ? styles5.ownBubble : styles5.otherBubble
2244
+ },
2245
+ children: [
2246
+ message.isPinned && /* @__PURE__ */ jsxs(
2247
+ "div",
2037
2248
  {
2038
- type: "button",
2039
- onClick: () => onRetry(message.id),
2040
- style: styles5.retryButton,
2041
- "aria-label": "Retry sending message",
2042
- children: "Retry"
2249
+ style: {
2250
+ ...styles5.pinnedBadge,
2251
+ ...isOwn ? styles5.pinnedBadgeOwn : styles5.pinnedBadgeOther
2252
+ },
2253
+ children: [
2254
+ /* @__PURE__ */ jsx(PinIcon, { size: 12 }),
2255
+ /* @__PURE__ */ jsx("span", { children: "Pinned" })
2256
+ ]
2043
2257
  }
2044
- )
2045
- ] }),
2046
- showSeenBy && isOwn && !message.status && /* @__PURE__ */ jsx(
2047
- SeenByIndicator,
2048
- {
2049
- readBy: message.readBy ?? [],
2050
- participants,
2051
- currentUserId,
2052
- senderId: message.senderId
2053
- }
2054
- )
2055
- ]
2056
- }
2057
- ) }),
2258
+ ),
2259
+ message.replyToSnapshot && /* @__PURE__ */ jsx(
2260
+ ReplyQuoteBlock,
2261
+ {
2262
+ snapshot: message.replyToSnapshot,
2263
+ inOwnBubble: isOwn,
2264
+ onClick: onReplyJumpTo
2265
+ }
2266
+ ),
2267
+ message.type === "text" && /* @__PURE__ */ jsx(TextContent, { body: message.body, onDeepLinkClick }),
2268
+ message.type === "audio" && /* @__PURE__ */ jsx(AudioContent, { mediaUrl: message.mediaUrl, duration: message.mediaDuration }),
2269
+ message.type === "image" && /* @__PURE__ */ jsx(ImageContent, { mediaUrl: message.mediaUrl, fileName: message.fileName }),
2270
+ message.type === "file" && /* @__PURE__ */ jsx(FileContent, { mediaUrl: message.mediaUrl, fileName: message.fileName }),
2271
+ message.type === "deep_link" && /* @__PURE__ */ jsx(DeepLinkContent, { body: message.body, metadata: message.metadata, onDeepLinkClick }),
2272
+ /* @__PURE__ */ jsxs("div", { style: styles5.bubbleFoot, children: [
2273
+ /* @__PURE__ */ jsx("span", { style: styles5.timestamp, children: formatTime(message.insertedAt) }),
2274
+ isOwn && message.status === "sending" && /* @__PURE__ */ jsxs("span", { style: styles5.footStatus, children: [
2275
+ /* @__PURE__ */ jsx(Spinner, { size: 11, color: "rgba(255,255,255,0.85)" }),
2276
+ /* @__PURE__ */ jsx("span", { children: "Sending\u2026" })
2277
+ ] }),
2278
+ isOwn && message.status === "failed" && /* @__PURE__ */ jsxs("span", { style: styles5.footStatusFailed, children: [
2279
+ /* @__PURE__ */ jsx(AlertIcon, { size: 11, color: "#fecaca" }),
2280
+ /* @__PURE__ */ jsx("span", { children: "Not sent" }),
2281
+ onRetry && /* @__PURE__ */ jsx(
2282
+ "button",
2283
+ {
2284
+ type: "button",
2285
+ onClick: () => onRetry(message.id),
2286
+ style: styles5.retryButton,
2287
+ "aria-label": "Retry sending message",
2288
+ children: "Retry"
2289
+ }
2290
+ )
2291
+ ] }),
2292
+ showSeenBy && isOwn && !message.status && /* @__PURE__ */ jsx(
2293
+ SeenByIndicator,
2294
+ {
2295
+ readBy: message.readBy ?? [],
2296
+ participants,
2297
+ currentUserId,
2298
+ senderId: message.senderId
2299
+ }
2300
+ )
2301
+ ] })
2302
+ ]
2303
+ }
2304
+ )
2305
+ ] }),
2058
2306
  !isOwn && actionsSlot
2059
2307
  ]
2060
2308
  }
2061
2309
  );
2062
2310
  }
2311
+ function computeInitials(name) {
2312
+ return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
2313
+ }
2314
+ function roleColor(role) {
2315
+ switch (role) {
2316
+ case "radiologist":
2317
+ return "#4f46e5";
2318
+ // indigo (blue-leaning)
2319
+ case "lab":
2320
+ return "#2563eb";
2321
+ // brand blue
2322
+ case "physician":
2323
+ return "#0284c7";
2324
+ // sky
2325
+ case "admin":
2326
+ return "#64748b";
2327
+ // slate (muted)
2328
+ default:
2329
+ return "#6b7280";
2330
+ }
2331
+ }
2063
2332
  function TextContent({ body, onDeepLinkClick }) {
2064
2333
  if (body.includes(DEEP_LINK_PREFIX)) {
2065
2334
  const parts = body.split(new RegExp(`(${escapeRegex2(DEEP_LINK_PREFIX)}[\\w/.-]+)`, "g"));
@@ -2121,19 +2390,13 @@ function DeepLinkContent({
2121
2390
  );
2122
2391
  }
2123
2392
  function SystemBubble({ message }) {
2124
- return /* @__PURE__ */ jsxs("div", { style: styles5.systemMessage, children: [
2393
+ return /* @__PURE__ */ jsx("div", { style: styles5.systemWrapper, children: /* @__PURE__ */ jsxs("span", { style: styles5.systemPill, children: [
2125
2394
  /* @__PURE__ */ jsx("span", { children: message.body }),
2126
2395
  /* @__PURE__ */ jsx("span", { style: styles5.systemTime, children: formatTime(message.insertedAt) })
2127
- ] });
2396
+ ] }) });
2128
2397
  }
2129
2398
  function RoleBadge({ role }) {
2130
- const colors = {
2131
- radiologist: "#7c3aed",
2132
- lab: "#2563eb",
2133
- physician: "#059669",
2134
- admin: "#dc2626"
2135
- };
2136
- return /* @__PURE__ */ jsx("span", { style: { ...styles5.roleBadge, backgroundColor: `${colors[role]}15`, color: colors[role] }, children: ROLE_LABELS[role] ?? role });
2399
+ return /* @__PURE__ */ jsx("span", { style: { ...styles5.roleBadge, backgroundColor: roleColor(role) }, children: ROLE_LABELS[role] ?? role });
2137
2400
  }
2138
2401
  function formatTime(iso) {
2139
2402
  try {
@@ -2155,89 +2418,134 @@ var styles5 = {
2155
2418
  display: "flex",
2156
2419
  alignItems: "flex-end",
2157
2420
  gap: SPACE.S1,
2158
- marginBottom: SPACE.S2,
2421
+ marginBottom: SPACE.S3,
2159
2422
  paddingLeft: SPACE.S4,
2160
2423
  paddingRight: SPACE.S4
2161
2424
  },
2162
2425
  bubbleWrapper: {
2163
2426
  position: "relative",
2164
- maxWidth: "75%"
2427
+ maxWidth: "78%",
2428
+ display: "flex",
2429
+ flexDirection: "column",
2430
+ gap: "4px"
2165
2431
  },
2166
2432
  actionsSlot: {
2167
2433
  flexShrink: 0,
2168
2434
  alignSelf: "flex-end"
2169
2435
  },
2436
+ // Author row above the bubble — only rendered for others' messages.
2437
+ authorRow: {
2438
+ display: "flex",
2439
+ alignItems: "center",
2440
+ gap: SPACE.S2
2441
+ },
2442
+ authorAvatar: {
2443
+ width: "36px",
2444
+ height: "36px",
2445
+ borderRadius: "50%",
2446
+ color: COLOR.white,
2447
+ fontSize: FONT_SIZE.sm,
2448
+ fontWeight: FONT_WEIGHT.bold,
2449
+ display: "flex",
2450
+ alignItems: "center",
2451
+ justifyContent: "center",
2452
+ flexShrink: 0,
2453
+ letterSpacing: "0.02em"
2454
+ },
2455
+ authorName: {
2456
+ fontSize: FONT_SIZE.sm,
2457
+ fontWeight: FONT_WEIGHT.semibold,
2458
+ color: COLOR.neutral700,
2459
+ overflow: "hidden",
2460
+ textOverflow: "ellipsis",
2461
+ whiteSpace: "nowrap",
2462
+ minWidth: 0
2463
+ },
2170
2464
  bubble: {
2171
2465
  padding: `${SPACE.S3} ${SPACE.S4}`,
2172
- borderRadius: RADIUS.lg,
2173
- wordBreak: "break-word"
2466
+ borderRadius: "18px",
2467
+ wordBreak: "break-word",
2468
+ border: "1px solid transparent"
2174
2469
  },
2175
2470
  ownBubble: {
2176
2471
  backgroundColor: COLOR.primary,
2177
2472
  color: COLOR.white,
2178
- borderBottomRightRadius: RADIUS.sm
2473
+ borderColor: COLOR.primary,
2474
+ borderBottomRightRadius: "6px"
2179
2475
  },
2180
2476
  otherBubble: {
2181
2477
  backgroundColor: COLOR.neutral100,
2182
2478
  color: COLOR.neutral900,
2183
- borderBottomLeftRadius: RADIUS.sm
2479
+ borderColor: COLOR.neutral200,
2480
+ borderBottomLeftRadius: "6px"
2184
2481
  },
2185
2482
  pinnedBadge: {
2186
2483
  display: "inline-flex",
2187
2484
  alignItems: "center",
2188
- gap: SPACE.S1,
2189
- fontSize: FONT_SIZE.xs,
2190
- fontWeight: FONT_WEIGHT.semibold,
2191
- marginBottom: SPACE.S1,
2192
- color: COLOR.neutral500
2485
+ gap: "4px",
2486
+ fontSize: "11px",
2487
+ fontWeight: FONT_WEIGHT.bold,
2488
+ marginBottom: SPACE.S2,
2489
+ padding: `2px ${SPACE.S2}`,
2490
+ borderRadius: "999px",
2491
+ textTransform: "uppercase",
2492
+ letterSpacing: "0.05em"
2193
2493
  },
2194
- senderRow: {
2195
- display: "flex",
2196
- alignItems: "center",
2197
- gap: SPACE.S2,
2198
- marginBottom: SPACE.S1
2494
+ pinnedBadgeOther: {
2495
+ color: "#b8680f",
2496
+ backgroundColor: "#fff7e0"
2199
2497
  },
2200
- senderName: {
2201
- fontSize: FONT_SIZE.sm,
2202
- fontWeight: FONT_WEIGHT.semibold,
2203
- color: COLOR.neutral700
2498
+ pinnedBadgeOwn: {
2499
+ color: "#fff4d6",
2500
+ backgroundColor: "rgba(255, 255, 255, 0.2)"
2204
2501
  },
2205
2502
  roleBadge: {
2206
- fontSize: FONT_SIZE.xs,
2207
- fontWeight: FONT_WEIGHT.semibold,
2208
- padding: `2px ${SPACE.S2}`,
2209
- borderRadius: RADIUS.sm
2503
+ fontSize: "10px",
2504
+ fontWeight: FONT_WEIGHT.bold,
2505
+ padding: "2px 7px",
2506
+ borderRadius: RADIUS.sm,
2507
+ textTransform: "uppercase",
2508
+ letterSpacing: "0.06em",
2509
+ color: COLOR.white,
2510
+ flexShrink: 0
2210
2511
  },
2211
2512
  textBody: {
2212
2513
  margin: 0,
2213
2514
  fontSize: FONT_SIZE.md,
2214
- lineHeight: LINE_HEIGHT.normal
2515
+ lineHeight: 1.5
2516
+ },
2517
+ bubbleFoot: {
2518
+ display: "flex",
2519
+ alignItems: "center",
2520
+ justifyContent: "flex-end",
2521
+ gap: SPACE.S2,
2522
+ marginTop: SPACE.S2,
2523
+ fontSize: "11px",
2524
+ opacity: 0.85
2215
2525
  },
2216
2526
  timestamp: {
2217
- fontSize: FONT_SIZE.xs,
2527
+ fontSize: "11px",
2218
2528
  color: "inherit",
2219
- opacity: 0.8,
2220
- marginTop: SPACE.S1,
2221
- textAlign: "right"
2529
+ fontVariantNumeric: "tabular-nums"
2222
2530
  },
2223
- statusRow: {
2224
- display: "flex",
2531
+ footStatus: {
2532
+ display: "inline-flex",
2225
2533
  alignItems: "center",
2226
- gap: SPACE.S2,
2227
- marginTop: SPACE.S1,
2228
- fontSize: FONT_SIZE.xs,
2229
- justifyContent: "flex-end",
2230
- color: "rgba(255, 255, 255, 0.9)"
2534
+ gap: "4px",
2535
+ fontSize: "11px"
2231
2536
  },
2232
- statusText: {
2233
- opacity: 0.9
2537
+ footStatusFailed: {
2538
+ display: "inline-flex",
2539
+ alignItems: "center",
2540
+ gap: "4px",
2541
+ fontSize: "11px"
2234
2542
  },
2235
2543
  retryButton: {
2236
2544
  background: "transparent",
2237
2545
  border: "none",
2238
2546
  color: COLOR.white,
2239
2547
  fontWeight: FONT_WEIGHT.semibold,
2240
- fontSize: FONT_SIZE.xs,
2548
+ fontSize: "11px",
2241
2549
  textDecoration: "underline",
2242
2550
  cursor: "pointer",
2243
2551
  padding: `0 ${SPACE.S1}`
@@ -2300,18 +2608,25 @@ var styles5 = {
2300
2608
  color: COLOR.neutral500,
2301
2609
  fontFamily: "monospace"
2302
2610
  },
2303
- systemMessage: {
2611
+ systemWrapper: {
2304
2612
  display: "flex",
2305
2613
  justifyContent: "center",
2614
+ margin: `${SPACE.S2} 0`
2615
+ },
2616
+ systemPill: {
2617
+ display: "inline-flex",
2306
2618
  alignItems: "center",
2307
2619
  gap: SPACE.S2,
2308
- padding: `${SPACE.S1} ${SPACE.S4}`,
2309
- fontSize: FONT_SIZE.sm,
2620
+ padding: `5px ${SPACE.S3}`,
2621
+ backgroundColor: COLOR.neutral100,
2622
+ borderRadius: "999px",
2623
+ fontSize: FONT_SIZE.xs,
2310
2624
  color: COLOR.neutral500
2311
2625
  },
2312
2626
  systemTime: {
2313
- fontSize: FONT_SIZE.xs,
2314
- color: COLOR.neutral400
2627
+ fontSize: "11px",
2628
+ color: COLOR.neutral400,
2629
+ fontVariantNumeric: "tabular-nums"
2315
2630
  }
2316
2631
  };
2317
2632
  function ChatIllustration({ size = 96 }) {
@@ -2450,6 +2765,7 @@ var MessageList = forwardRef(function MessageList2({
2450
2765
  const containerRef = useRef(null);
2451
2766
  const bottomRef = useRef(null);
2452
2767
  const prevMessageCount = useRef(messages.length);
2768
+ const hasInitiallyScrolledRef = useRef(false);
2453
2769
  useImperativeHandle(
2454
2770
  ref,
2455
2771
  () => ({
@@ -2467,7 +2783,31 @@ var MessageList = forwardRef(function MessageList2({
2467
2783
  }),
2468
2784
  []
2469
2785
  );
2786
+ useLayoutEffect(() => {
2787
+ const container = containerRef.current;
2788
+ if (!container || messages.length === 0) return;
2789
+ if (hasInitiallyScrolledRef.current) return;
2790
+ hasInitiallyScrolledRef.current = true;
2791
+ prevMessageCount.current = messages.length;
2792
+ const firstUnread = messages.find(
2793
+ (m) => m.type !== "system" && m.senderId !== currentUserId && !(m.readBy ?? []).includes(currentUserId)
2794
+ );
2795
+ if (firstUnread) {
2796
+ const el = container.querySelector(
2797
+ `[data-message-id="${firstUnread.id}"]`
2798
+ );
2799
+ if (el) {
2800
+ const containerRect = container.getBoundingClientRect();
2801
+ const elRect = el.getBoundingClientRect();
2802
+ const offset = elRect.top - containerRect.top + container.scrollTop;
2803
+ container.scrollTop = Math.max(0, offset - 16);
2804
+ return;
2805
+ }
2806
+ }
2807
+ container.scrollTop = container.scrollHeight;
2808
+ }, [messages, currentUserId]);
2470
2809
  useEffect(() => {
2810
+ if (!hasInitiallyScrolledRef.current) return;
2471
2811
  if (messages.length > prevMessageCount.current) {
2472
2812
  const lastMessage = messages[messages.length - 1];
2473
2813
  const isOwnMessage = lastMessage?.senderId === currentUserId;
@@ -2526,7 +2866,11 @@ var MessageList = forwardRef(function MessageList2({
2526
2866
  const showDivider = !!currentBucket && currentBucket !== lastValidBucket;
2527
2867
  if (currentBucket) lastValidBucket = currentBucket;
2528
2868
  return /* @__PURE__ */ jsxs(React4.Fragment, { children: [
2529
- showDivider && /* @__PURE__ */ jsx("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: /* @__PURE__ */ jsx("span", { style: styles6.dayDividerPill, children: formatDayDivider(message.insertedAt) }) }),
2869
+ showDivider && /* @__PURE__ */ jsxs("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: [
2870
+ /* @__PURE__ */ jsx("span", { style: styles6.dayHr, "aria-hidden": "true" }),
2871
+ /* @__PURE__ */ jsx("span", { style: styles6.dayLabel, children: formatDayDivider(message.insertedAt) }),
2872
+ /* @__PURE__ */ jsx("span", { style: styles6.dayHr, "aria-hidden": "true" })
2873
+ ] }),
2530
2874
  /* @__PURE__ */ jsx("div", { "data-message-id": message.id, "data-natoe-message-bubble": true, style: styles6.bubbleSlot, children: /* @__PURE__ */ jsx(
2531
2875
  MessageBubble,
2532
2876
  {
@@ -2625,23 +2969,32 @@ var styles6 = {
2625
2969
  },
2626
2970
  dayDivider: {
2627
2971
  display: "flex",
2628
- justifyContent: "center",
2629
- margin: `${SPACE.S3} 0 ${SPACE.S2}`,
2972
+ alignItems: "center",
2973
+ gap: SPACE.S3,
2974
+ margin: `${SPACE.S4} 0 ${SPACE.S3}`,
2630
2975
  position: "sticky",
2631
2976
  top: SPACE.S1,
2632
2977
  zIndex: Z_INDEX.sticky,
2633
2978
  pointerEvents: "none"
2634
2979
  },
2635
- dayDividerPill: {
2636
- display: "inline-block",
2637
- padding: `${SPACE.S1} ${SPACE.S3}`,
2638
- fontSize: FONT_SIZE.sm,
2639
- fontWeight: FONT_WEIGHT.medium,
2640
- color: COLOR.slate700,
2641
- backgroundColor: COLOR.slate200,
2642
- borderRadius: RADIUS.lg,
2643
- letterSpacing: "0.2px",
2644
- boxShadow: SHADOW.subtle
2980
+ dayHr: {
2981
+ flex: 1,
2982
+ height: "1px",
2983
+ backgroundColor: COLOR.neutral200
2984
+ },
2985
+ dayLabel: {
2986
+ flexShrink: 0,
2987
+ fontSize: FONT_SIZE.xs,
2988
+ fontWeight: FONT_WEIGHT.semibold,
2989
+ color: COLOR.neutral500,
2990
+ textTransform: "uppercase",
2991
+ letterSpacing: "0.06em",
2992
+ // Subtle pill background so the label is readable when it overlays
2993
+ // bubbles passing under it during sticky scroll. Without this the
2994
+ // hairlines visually run through the text on tinted backgrounds.
2995
+ padding: `2px ${SPACE.S2}`,
2996
+ backgroundColor: COLOR.white,
2997
+ borderRadius: RADIUS.sm
2645
2998
  },
2646
2999
  bubbleSlot: {
2647
3000
  animation: "natoe-colab-message-in 180ms ease-out"
@@ -2895,11 +3248,11 @@ function MessageInput({
2895
3248
  {
2896
3249
  onClick: () => fileInputRef.current?.click(),
2897
3250
  disabled: disabled || isSending,
2898
- style: styles8.iconButton,
3251
+ style: styles8.attachButton,
2899
3252
  "aria-label": "Attach file",
2900
3253
  title: "Attach file",
2901
3254
  type: "button",
2902
- children: /* @__PURE__ */ jsx(AttachIcon, { size: 22, color: "#374151" })
3255
+ children: /* @__PURE__ */ jsx(AttachIcon, { size: 22, color: COLOR.neutral700 })
2903
3256
  }
2904
3257
  ),
2905
3258
  /* @__PURE__ */ jsx(
@@ -2938,7 +3291,7 @@ function MessageInput({
2938
3291
  style: styles8.textarea
2939
3292
  }
2940
3293
  ),
2941
- /* @__PURE__ */ jsx(
3294
+ /* @__PURE__ */ jsxs(
2942
3295
  "button",
2943
3296
  {
2944
3297
  onClick: handleSendText,
@@ -2947,10 +3300,13 @@ function MessageInput({
2947
3300
  title: "Send message",
2948
3301
  style: {
2949
3302
  ...styles8.sendButton,
2950
- opacity: text.trim() ? 1 : 0.4
3303
+ opacity: text.trim() ? 1 : 0.5
2951
3304
  },
2952
3305
  type: "button",
2953
- children: /* @__PURE__ */ jsx(SendIcon, { size: 20, color: COLOR.white })
3306
+ children: [
3307
+ /* @__PURE__ */ jsx(SendIcon, { size: 20, color: COLOR.white }),
3308
+ /* @__PURE__ */ jsx("span", { style: styles8.sendLabel, children: "Send" })
3309
+ ]
2954
3310
  }
2955
3311
  )
2956
3312
  ] })
@@ -3012,46 +3368,64 @@ var styles8 = {
3012
3368
  inputBar: {
3013
3369
  display: "flex",
3014
3370
  alignItems: "flex-end",
3015
- gap: SPACE.S2,
3016
- padding: `${SPACE.S3} ${SPACE.S3}`
3017
- },
3018
- iconButton: {
3019
- width: SIZE.controlPrimary,
3020
- height: SIZE.controlPrimary,
3371
+ gap: "10px",
3372
+ padding: `${SPACE.S3} ${SPACE.S4} 14px`
3373
+ },
3374
+ // 48px square tile with a 14px corner radius — visually grounds the
3375
+ // attach affordance as part of the composer chrome rather than a stray
3376
+ // round icon button.
3377
+ attachButton: {
3378
+ width: "48px",
3379
+ height: "48px",
3021
3380
  display: "flex",
3022
3381
  alignItems: "center",
3023
3382
  justifyContent: "center",
3024
- backgroundColor: "transparent",
3025
- border: "none",
3026
- borderRadius: RADIUS.full,
3383
+ backgroundColor: COLOR.neutral100,
3384
+ border: `1.5px solid ${COLOR.neutral200}`,
3385
+ borderRadius: "14px",
3027
3386
  cursor: "pointer",
3028
- flexShrink: 0
3387
+ flexShrink: 0,
3388
+ color: COLOR.neutral700,
3389
+ transition: "background-color 120ms ease, color 120ms ease, border-color 120ms ease"
3029
3390
  },
3030
3391
  textarea: {
3031
3392
  flex: 1,
3032
- padding: `${SPACE.S3} ${SPACE.S4}`,
3393
+ minHeight: "48px",
3394
+ maxHeight: "140px",
3395
+ padding: "13px 16px",
3033
3396
  fontSize: FONT_SIZE.md,
3034
3397
  lineHeight: LINE_HEIGHT.normal,
3035
- border: `1px solid ${COLOR.neutral300}`,
3036
- borderRadius: RADIUS.pill,
3398
+ color: COLOR.neutral900,
3399
+ backgroundColor: COLOR.neutral100,
3400
+ border: `1.5px solid ${COLOR.neutral200}`,
3401
+ borderRadius: "14px",
3037
3402
  resize: "none",
3038
3403
  outline: "none",
3039
3404
  fontFamily: "inherit",
3040
- maxHeight: "120px",
3041
- minHeight: SIZE.controlPrimary
3405
+ transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease"
3042
3406
  },
3407
+ // Pill-shaped send: icon + "Send" label. Always labelled (never icon-
3408
+ // only) so the affordance is obvious to first-time users.
3043
3409
  sendButton: {
3044
- width: SIZE.controlPrimary,
3045
- height: SIZE.controlPrimary,
3046
- display: "flex",
3410
+ display: "inline-flex",
3047
3411
  alignItems: "center",
3048
- justifyContent: "center",
3412
+ gap: "6px",
3413
+ minWidth: "48px",
3414
+ height: "48px",
3415
+ padding: `0 ${SPACE.S4}`,
3049
3416
  backgroundColor: COLOR.primary,
3050
3417
  color: COLOR.white,
3051
3418
  border: "none",
3052
- borderRadius: RADIUS.full,
3419
+ borderRadius: "14px",
3053
3420
  cursor: "pointer",
3054
- flexShrink: 0
3421
+ flexShrink: 0,
3422
+ fontFamily: "inherit",
3423
+ fontSize: "15px",
3424
+ fontWeight: FONT_WEIGHT.semibold,
3425
+ transition: "opacity 120ms ease, background-color 120ms ease"
3426
+ },
3427
+ sendLabel: {
3428
+ lineHeight: 1
3055
3429
  }};
3056
3430
  function ParticipantsList({
3057
3431
  participants,
@@ -3108,10 +3482,14 @@ function fallbackName(role) {
3108
3482
  }
3109
3483
  function RoleBadge2({ role }) {
3110
3484
  const colors = {
3111
- radiologist: { bg: "#f5f3ff", text: "#7c3aed" },
3485
+ radiologist: { bg: "#eef2ff", text: "#4f46e5" },
3486
+ // indigo
3112
3487
  lab: { bg: "#eff6ff", text: "#2563eb" },
3113
- physician: { bg: "#ecfdf5", text: "#059669" },
3114
- admin: { bg: "#fef2f2", text: "#dc2626" }
3488
+ // brand blue
3489
+ physician: { bg: "#e0f2fe", text: "#0284c7" },
3490
+ // sky
3491
+ admin: { bg: "#f1f5f9", text: "#64748b" }
3492
+ // slate
3115
3493
  };
3116
3494
  const color = colors[role];
3117
3495
  return /* @__PURE__ */ jsx("span", { style: { ...styles9.roleBadge, backgroundColor: color.bg, color: color.text }, children: role });
@@ -3720,46 +4098,21 @@ var styles12 = {
3720
4098
  letterSpacing: "0.4px"
3721
4099
  }
3722
4100
  };
3723
-
3724
- // src/core/styles.ts
3725
- var FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
3726
- var ROOT_CLASS = "natoe-colab-root";
3727
- var GLOBAL_STYLE_ID = "natoe-colab-global-styles";
3728
- var GLOBAL_CSS = `
3729
- @keyframes natoe-colab-spin { to { transform: rotate(360deg); } }
3730
-
3731
- @keyframes natoe-colab-message-in {
3732
- from { opacity: 0; transform: translateY(4px); }
3733
- to { opacity: 1; transform: translateY(0); }
3734
- }
3735
-
3736
- /* High-contrast adjustments \u2014 older users on OS-level high-contrast mode
3737
- get heavier borders and stronger text without losing the package look. */
3738
- @media (prefers-contrast: more) {
3739
- .${ROOT_CLASS} {
3740
- color: #000000;
3741
- }
3742
- .${ROOT_CLASS} button {
3743
- outline: 1px solid currentColor;
3744
- }
3745
- }
3746
-
3747
- /* Honour reduced-motion preferences by killing entry animations. */
3748
- @media (prefers-reduced-motion: reduce) {
3749
- .${ROOT_CLASS} [data-natoe-message-bubble] {
3750
- animation: none !important;
3751
- }
3752
- }
3753
- `;
3754
- function ensureGlobalStyles() {
3755
- if (typeof document === "undefined") return;
3756
- if (document.getElementById(GLOBAL_STYLE_ID)) return;
3757
- const style = document.createElement("style");
3758
- style.id = GLOBAL_STYLE_ID;
3759
- style.textContent = GLOBAL_CSS;
3760
- document.head.appendChild(style);
3761
- }
3762
4101
  ensureGlobalStyles();
4102
+ var DARK_THEME_OVERRIDES = {
4103
+ ["--natoe-colab-white"]: "#0b0b0c",
4104
+ ["--natoe-colab-neutral-50"]: "#18181c",
4105
+ ["--natoe-colab-neutral-100"]: "#1f1f23",
4106
+ ["--natoe-colab-neutral-200"]: "#2a2a2e",
4107
+ ["--natoe-colab-neutral-300"]: "#3f3f44",
4108
+ ["--natoe-colab-neutral-400"]: "#6b7280",
4109
+ ["--natoe-colab-neutral-500"]: "#9ca3af",
4110
+ ["--natoe-colab-neutral-600"]: "#cbd5e1",
4111
+ ["--natoe-colab-neutral-700"]: "#e5e7eb",
4112
+ ["--natoe-colab-neutral-800"]: "#f3f4f6",
4113
+ ["--natoe-colab-neutral-900"]: "#ffffff",
4114
+ ["--natoe-colab-primary-bg"]: "rgba(37, 99, 235, 0.18)"
4115
+ };
3763
4116
  function CollabPanel({
3764
4117
  orderId,
3765
4118
  patientData,
@@ -3768,6 +4121,7 @@ function CollabPanel({
3768
4121
  onBack,
3769
4122
  hidePatientName = false,
3770
4123
  onConversationChange,
4124
+ themeMode = "light",
3771
4125
  className,
3772
4126
  style
3773
4127
  }) {
@@ -3843,16 +4197,21 @@ function CollabPanel({
3843
4197
  }
3844
4198
  };
3845
4199
  const pinDisabled = pinnedMessages.length >= MAX_PINNED_MESSAGES;
4200
+ const containerStyle = {
4201
+ ...panelStyles.container,
4202
+ ...themeMode === "dark" ? DARK_THEME_OVERRIDES : {},
4203
+ ...style
4204
+ };
3846
4205
  if (error) {
3847
- return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxs("div", { style: panelStyles.errorState, children: [
4206
+ return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsxs("div", { style: panelStyles.errorState, children: [
3848
4207
  /* @__PURE__ */ jsx("p", { style: panelStyles.errorTitle, children: "Unable to load conversation" }),
3849
4208
  /* @__PURE__ */ jsx("p", { style: panelStyles.errorMessage, children: error })
3850
4209
  ] }) });
3851
4210
  }
3852
4211
  if (isLoading && !conversation) {
3853
- return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
4212
+ return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
3854
4213
  }
3855
- return /* @__PURE__ */ jsxs("div", { className, style: { ...panelStyles.container, ...style }, children: [
4214
+ return /* @__PURE__ */ jsxs("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: [
3856
4215
  showSettings && conversation ? /* @__PURE__ */ jsx(
3857
4216
  ChannelSettings,
3858
4217
  {
@@ -4007,6 +4366,7 @@ function CollabPopup({
4007
4366
  isOpen,
4008
4367
  onClose,
4009
4368
  onBack,
4369
+ onMinimize,
4010
4370
  initialPosition,
4011
4371
  width = 380,
4012
4372
  height = 520,
@@ -4122,35 +4482,48 @@ function CollabPopup({
4122
4482
  "button",
4123
4483
  {
4124
4484
  onClick: onBack,
4125
- style: styles13.titleButton,
4126
- "aria-label": "Go back",
4485
+ style: styles13.titleSquare,
4486
+ "aria-label": "Back to messages",
4127
4487
  title: "Back",
4128
4488
  type: "button",
4129
- children: "\u2190"
4489
+ children: /* @__PURE__ */ jsx(BackIcon, { size: 18, color: COLOR.neutral700 })
4130
4490
  }
4131
4491
  ),
4132
- /* @__PURE__ */ jsx("span", { id: titleId, style: { ...styles13.titleText, flex: 1 }, children: titleText }),
4492
+ /* @__PURE__ */ jsxs("div", { style: styles13.titleCenter, children: [
4493
+ /* @__PURE__ */ jsx("h2", { id: titleId, style: styles13.patientName, children: patientData.patientName || titleText }),
4494
+ (patientData.labName || patientData.displayOrderId) && /* @__PURE__ */ jsxs("div", { style: styles13.subRow, children: [
4495
+ patientData.labName && /* @__PURE__ */ jsx("span", { children: patientData.labName }),
4496
+ patientData.labName && patientData.displayOrderId && /* @__PURE__ */ jsx("span", { style: styles13.subDot, "aria-hidden": "true", children: "\xB7" }),
4497
+ patientData.displayOrderId && /* @__PURE__ */ jsxs("span", { style: styles13.mono, children: [
4498
+ "#",
4499
+ patientData.displayOrderId.slice(-4)
4500
+ ] })
4501
+ ] })
4502
+ ] }),
4133
4503
  /* @__PURE__ */ jsxs("div", { style: styles13.titleActions, children: [
4134
4504
  /* @__PURE__ */ jsx(
4135
4505
  "button",
4136
4506
  {
4137
- onClick: () => setIsMinimized(!isMinimized),
4138
- style: styles13.titleButton,
4139
- "aria-label": isMinimized ? "Expand chat" : "Minimize chat",
4140
- title: isMinimized ? "Expand" : "Minimize",
4507
+ onClick: () => {
4508
+ if (onMinimize) onMinimize();
4509
+ else setIsMinimized(!isMinimized);
4510
+ },
4511
+ style: styles13.titleSquare,
4512
+ "aria-label": onMinimize ? "Minimize chat" : isMinimized ? "Expand chat" : "Minimize chat",
4513
+ title: onMinimize ? "Minimize" : isMinimized ? "Expand" : "Minimize",
4141
4514
  type: "button",
4142
- children: isMinimized ? /* @__PURE__ */ jsx(ExpandIcon, { size: 16, color: COLOR.slate200 }) : /* @__PURE__ */ jsx(MinimizeIcon, { size: 16, color: COLOR.slate200 })
4515
+ children: onMinimize || !isMinimized ? /* @__PURE__ */ jsx(MinimizeIcon, { size: 18, color: COLOR.neutral700 }) : /* @__PURE__ */ jsx(ExpandIcon, { size: 18, color: COLOR.neutral700 })
4143
4516
  }
4144
4517
  ),
4145
4518
  /* @__PURE__ */ jsx(
4146
4519
  "button",
4147
4520
  {
4148
4521
  onClick: onClose,
4149
- style: styles13.titleButton,
4522
+ style: styles13.titleSquare,
4150
4523
  "aria-label": "Close chat",
4151
4524
  title: "Close",
4152
4525
  type: "button",
4153
- children: /* @__PURE__ */ jsx(CloseIcon, { size: 16, color: COLOR.slate200 })
4526
+ children: /* @__PURE__ */ jsx(CloseIcon, { size: 18, color: COLOR.neutral700 })
4154
4527
  }
4155
4528
  )
4156
4529
  ] })
@@ -4173,51 +4546,78 @@ var styles13 = {
4173
4546
  container: {
4174
4547
  position: "fixed",
4175
4548
  zIndex: Z_INDEX.dialog,
4176
- borderRadius: RADIUS.lg,
4177
- boxShadow: SHADOW.dialog,
4549
+ borderRadius: "20px",
4550
+ boxShadow: "0 20px 48px rgba(20, 21, 28, 0.16), 0 6px 16px rgba(20, 21, 28, 0.08)",
4178
4551
  overflow: "hidden",
4179
4552
  display: "flex",
4180
4553
  flexDirection: "column",
4181
4554
  backgroundColor: COLOR.white,
4182
- border: `1px solid ${COLOR.neutral200}`,
4555
+ border: `1px solid ${COLOR.neutral300}`,
4183
4556
  transition: "height 0.2s ease",
4184
4557
  fontFamily: FONT_STACK
4185
4558
  },
4186
4559
  titleBar: {
4187
4560
  display: "flex",
4188
- justifyContent: "space-between",
4189
4561
  alignItems: "center",
4190
- padding: `0 ${SPACE.S2} 0 ${SPACE.S4}`,
4191
- height: "52px",
4192
- backgroundColor: COLOR.slate800,
4562
+ gap: SPACE.S2,
4563
+ padding: `10px ${SPACE.S3}`,
4564
+ backgroundColor: COLOR.white,
4565
+ borderBottom: `1px solid ${COLOR.neutral200}`,
4193
4566
  cursor: "grab",
4194
4567
  userSelect: "none",
4195
4568
  flexShrink: 0
4196
4569
  },
4197
- titleText: {
4198
- fontSize: FONT_SIZE.md,
4199
- fontWeight: FONT_WEIGHT.medium,
4200
- color: COLOR.white,
4570
+ titleCenter: {
4571
+ flex: 1,
4572
+ minWidth: 0,
4573
+ display: "flex",
4574
+ flexDirection: "column",
4575
+ gap: "2px"
4576
+ },
4577
+ patientName: {
4578
+ margin: 0,
4579
+ fontSize: FONT_SIZE.lg,
4580
+ fontWeight: FONT_WEIGHT.bold,
4581
+ letterSpacing: "-0.01em",
4582
+ color: COLOR.neutral900,
4201
4583
  overflow: "hidden",
4202
4584
  textOverflow: "ellipsis",
4203
4585
  whiteSpace: "nowrap"
4204
4586
  },
4587
+ subRow: {
4588
+ display: "flex",
4589
+ alignItems: "center",
4590
+ gap: SPACE.S2,
4591
+ fontSize: FONT_SIZE.sm,
4592
+ color: COLOR.neutral500,
4593
+ overflow: "hidden",
4594
+ textOverflow: "ellipsis",
4595
+ whiteSpace: "nowrap"
4596
+ },
4597
+ subDot: {
4598
+ color: COLOR.neutral400
4599
+ },
4600
+ mono: {
4601
+ fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4602
+ fontVariantNumeric: "tabular-nums"
4603
+ },
4205
4604
  titleActions: {
4206
4605
  display: "flex",
4207
4606
  gap: SPACE.S1,
4208
4607
  flexShrink: 0
4209
4608
  },
4210
- titleButton: {
4211
- width: SIZE.control,
4212
- height: SIZE.control,
4609
+ titleSquare: {
4610
+ width: "36px",
4611
+ height: "36px",
4213
4612
  display: "flex",
4214
4613
  alignItems: "center",
4215
4614
  justifyContent: "center",
4216
- backgroundColor: "transparent",
4217
- border: "none",
4615
+ backgroundColor: COLOR.neutral100,
4616
+ border: `1px solid ${COLOR.neutral200}`,
4218
4617
  borderRadius: RADIUS.md,
4219
4618
  cursor: "pointer",
4220
- color: COLOR.slate200
4619
+ color: COLOR.neutral700,
4620
+ flexShrink: 0
4221
4621
  },
4222
4622
  panelWrapper: {
4223
4623
  flex: 1,
@@ -4247,6 +4647,7 @@ function useInlineCollab({
4247
4647
  const elementRef = useRef(null);
4248
4648
  const observerRef = useRef(null);
4249
4649
  const subscribedConversationIdRef = useRef(null);
4650
+ const channelSubscriptionRef = useRef(null);
4250
4651
  const trimToLimit = useCallback(
4251
4652
  (msgs) => msgs.length > messageLimit ? msgs.slice(msgs.length - messageLimit) : msgs,
4252
4653
  [messageLimit]
@@ -4284,7 +4685,9 @@ function useInlineCollab({
4284
4685
  const subscribe = useCallback(
4285
4686
  (conversationId) => {
4286
4687
  if (subscribedConversationIdRef.current === conversationId) return;
4287
- socket.joinConversation(conversationId, {
4688
+ channelSubscriptionRef.current?.release();
4689
+ channelSubscriptionRef.current = null;
4690
+ const subscription = socket.joinConversation(conversationId, {
4288
4691
  onMessage: (msg) => {
4289
4692
  setMessages((prev) => trimToLimit([...prev, msg]));
4290
4693
  if (msg.senderId !== config.userId) {
@@ -4300,19 +4703,18 @@ function useInlineCollab({
4300
4703
  setParticipants((prev) => prev.filter((p) => p.userId !== participant.userId));
4301
4704
  }
4302
4705
  });
4706
+ channelSubscriptionRef.current = subscription;
4303
4707
  subscribedConversationIdRef.current = conversationId;
4304
4708
  setIsSubscribed(true);
4305
4709
  },
4306
4710
  [socket, trimToLimit, config.userId]
4307
4711
  );
4308
4712
  const unsubscribe = useCallback(() => {
4309
- const convId = subscribedConversationIdRef.current;
4310
- if (convId) {
4311
- socket.leaveConversation(convId);
4312
- subscribedConversationIdRef.current = null;
4313
- setIsSubscribed(false);
4314
- }
4315
- }, [socket]);
4713
+ channelSubscriptionRef.current?.release();
4714
+ channelSubscriptionRef.current = null;
4715
+ subscribedConversationIdRef.current = null;
4716
+ setIsSubscribed(false);
4717
+ }, []);
4316
4718
  const containerRef = useCallback(
4317
4719
  (element) => {
4318
4720
  if (observerRef.current) {
@@ -4454,16 +4856,52 @@ function useInlineCollab({
4454
4856
  };
4455
4857
  }
4456
4858
  ensureGlobalStyles();
4859
+ function palette(mode) {
4860
+ if (mode === "dark") {
4861
+ return {
4862
+ bg: "transparent",
4863
+ border: "transparent",
4864
+ previewBorder: "#1f1f23",
4865
+ senderName: "#f3f4f6",
4866
+ messageText: "#cbd5e1",
4867
+ systemText: "#6b7280",
4868
+ inputBg: "#18181c",
4869
+ inputBorder: "#2a2a2e",
4870
+ inputText: "#e5e7eb",
4871
+ audioBg: "rgba(37, 99, 235, 0.18)",
4872
+ audioFg: "#93c5fd",
4873
+ expandBorder: "#2a2a2e",
4874
+ expandColor: "#9ca3af"
4875
+ };
4876
+ }
4877
+ return {
4878
+ bg: COLOR.white,
4879
+ border: COLOR.neutral200,
4880
+ previewBorder: COLOR.neutral100,
4881
+ senderName: COLOR.neutral700,
4882
+ messageText: COLOR.neutral600,
4883
+ systemText: COLOR.neutral400,
4884
+ inputBg: "transparent",
4885
+ inputBorder: COLOR.neutral200,
4886
+ inputText: COLOR.neutral900,
4887
+ audioBg: COLOR.primaryBg,
4888
+ audioFg: COLOR.primary,
4889
+ expandBorder: COLOR.neutral200,
4890
+ expandColor: COLOR.neutral500
4891
+ };
4892
+ }
4457
4893
  function CollabInline({
4458
4894
  orderId,
4459
4895
  patientData,
4460
4896
  participantIds,
4461
4897
  onExpand,
4462
- messageLimit = 5,
4898
+ messageLimit = 1,
4463
4899
  placeholder = "Type a message about this case...",
4900
+ mode = "light",
4464
4901
  className,
4465
4902
  style
4466
4903
  }) {
4904
+ const pal = palette(mode);
4467
4905
  const {
4468
4906
  hasConversation,
4469
4907
  messages,
@@ -4475,11 +4913,17 @@ function CollabInline({
4475
4913
  sendAudioMessage,
4476
4914
  containerRef
4477
4915
  } = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
4916
+ const containerStyle = {
4917
+ ...styles14.container,
4918
+ backgroundColor: pal.bg,
4919
+ borderColor: pal.border,
4920
+ ...style
4921
+ };
4478
4922
  if (isLoading && !hasConversation) {
4479
- return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: /* @__PURE__ */ jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4923
+ return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: /* @__PURE__ */ jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4480
4924
  }
4481
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: [
4482
- hasConversation && messages.length > 0 && /* @__PURE__ */ jsx("div", { style: styles14.preview, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsx(InlineMessageRow, { message }, message.id)) }),
4925
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: containerStyle, children: [
4926
+ hasConversation && messages.length > 0 && /* @__PURE__ */ jsx("div", { style: { ...styles14.preview, borderBottomColor: pal.previewBorder }, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsx(InlineMessageRow, { message, pal }, message.id)) }),
4483
4927
  error && /* @__PURE__ */ jsx("div", { style: styles14.error, children: error }),
4484
4928
  /* @__PURE__ */ jsx(
4485
4929
  InlineInputBar,
@@ -4490,45 +4934,54 @@ function CollabInline({
4490
4934
  unreadCount: hasConversation ? unreadCount : 0,
4491
4935
  isLive: isSubscribed,
4492
4936
  showExpand: hasConversation && !!onExpand,
4493
- onExpand
4937
+ onExpand,
4938
+ pal
4494
4939
  }
4495
4940
  )
4496
4941
  ] });
4497
4942
  }
4498
- function InlineMessageRow({ message }) {
4943
+ function InlineMessageRow({ message, pal }) {
4499
4944
  if (message.type === "system") {
4500
- return /* @__PURE__ */ jsx("div", { style: styles14.systemRow, children: /* @__PURE__ */ jsx("span", { style: styles14.systemText, children: message.body }) });
4945
+ return /* @__PURE__ */ jsx("div", { style: styles14.systemRow, children: /* @__PURE__ */ jsx("span", { style: { ...styles14.systemText, color: pal.systemText }, children: message.body }) });
4501
4946
  }
4502
4947
  if (message.type === "audio" && message.mediaUrl) {
4503
4948
  return /* @__PURE__ */ jsxs("div", { style: styles14.messageRow, children: [
4504
4949
  /* @__PURE__ */ jsx(RoleDot, { role: message.senderRole }),
4505
- /* @__PURE__ */ jsxs("span", { style: styles14.senderName, children: [
4950
+ /* @__PURE__ */ jsxs("span", { style: { ...styles14.senderName, color: pal.senderName }, children: [
4506
4951
  message.senderName,
4507
4952
  ":"
4508
4953
  ] }),
4509
- /* @__PURE__ */ jsx(InlineAudioPlayer, { url: message.mediaUrl, duration: message.mediaDuration })
4954
+ /* @__PURE__ */ jsx(InlineAudioPlayer, { url: message.mediaUrl, duration: message.mediaDuration, pal })
4510
4955
  ] });
4511
4956
  }
4512
4957
  const preview = renderMessagePreview(message);
4513
4958
  return /* @__PURE__ */ jsxs("div", { style: styles14.messageRow, children: [
4514
4959
  /* @__PURE__ */ jsx(RoleDot, { role: message.senderRole }),
4515
- /* @__PURE__ */ jsxs("span", { style: styles14.senderName, children: [
4960
+ /* @__PURE__ */ jsxs("span", { style: { ...styles14.senderName, color: pal.senderName }, children: [
4516
4961
  message.senderName,
4517
4962
  ":"
4518
4963
  ] }),
4519
- /* @__PURE__ */ jsx("span", { style: styles14.messageText, children: preview })
4964
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.messageText, color: pal.messageText }, children: preview })
4520
4965
  ] });
4521
4966
  }
4522
4967
  function RoleDot({ role }) {
4523
4968
  const colors = {
4524
- radiologist: "#7c3aed",
4969
+ radiologist: "#4f46e5",
4970
+ // indigo
4525
4971
  lab: "#2563eb",
4526
- physician: "#059669",
4527
- admin: "#dc2626"
4972
+ // brand blue
4973
+ physician: "#0284c7",
4974
+ // sky
4975
+ admin: "#64748b"
4976
+ // slate
4528
4977
  };
4529
4978
  return /* @__PURE__ */ jsx("span", { style: { ...styles14.roleDot, backgroundColor: colors[role] } });
4530
4979
  }
4531
- function InlineAudioPlayer({ url, duration }) {
4980
+ function InlineAudioPlayer({
4981
+ url,
4982
+ duration,
4983
+ pal
4984
+ }) {
4532
4985
  const audioRef = useRef(null);
4533
4986
  const [isPlaying, setIsPlaying] = useState(false);
4534
4987
  const [currentTime, setCurrentTime] = useState(0);
@@ -4570,7 +5023,7 @@ function InlineAudioPlayer({ url, duration }) {
4570
5023
  const total = duration ?? 0;
4571
5024
  const remaining = Math.max(0, total - Math.floor(currentTime));
4572
5025
  const displayTime = isPlaying ? remaining : total;
4573
- return /* @__PURE__ */ jsxs("span", { style: styles14.audioPlayer, children: [
5026
+ return /* @__PURE__ */ jsxs("span", { style: { ...styles14.audioPlayer, backgroundColor: pal.audioBg, borderColor: pal.audioBg }, children: [
4574
5027
  /* @__PURE__ */ jsx(
4575
5028
  "button",
4576
5029
  {
@@ -4582,15 +5035,15 @@ function InlineAudioPlayer({ url, duration }) {
4582
5035
  }
4583
5036
  ),
4584
5037
  /* @__PURE__ */ jsxs("span", { style: styles14.audioWaveform, children: [
4585
- /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%" } }),
4586
- /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "80%" } }),
4587
- /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "60%" } }),
4588
- /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "90%" } }),
4589
- /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "50%" } }),
4590
- /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "70%" } }),
4591
- /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%" } })
5038
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%", backgroundColor: pal.audioFg } }),
5039
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "80%", backgroundColor: pal.audioFg } }),
5040
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "60%", backgroundColor: pal.audioFg } }),
5041
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "90%", backgroundColor: pal.audioFg } }),
5042
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "50%", backgroundColor: pal.audioFg } }),
5043
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "70%", backgroundColor: pal.audioFg } }),
5044
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.waveBar, height: "40%", backgroundColor: pal.audioFg } })
4592
5045
  ] }),
4593
- /* @__PURE__ */ jsx("span", { style: styles14.audioDuration, children: formatDuration2(displayTime) }),
5046
+ /* @__PURE__ */ jsx("span", { style: { ...styles14.audioDuration, color: pal.audioFg }, children: formatDuration2(displayTime) }),
4594
5047
  /* @__PURE__ */ jsx("audio", { ref: audioRef, src: url, preload: "metadata" })
4595
5048
  ] });
4596
5049
  }
@@ -4620,7 +5073,8 @@ function InlineInputBar({
4620
5073
  unreadCount,
4621
5074
  isLive,
4622
5075
  showExpand,
4623
- onExpand
5076
+ onExpand,
5077
+ pal
4624
5078
  }) {
4625
5079
  const [text, setText] = useState("");
4626
5080
  const [isSending, setIsSending] = useState(false);
@@ -4657,7 +5111,12 @@ function InlineInputBar({
4657
5111
  onKeyDown: handleKeyDown,
4658
5112
  placeholder,
4659
5113
  disabled: isSending,
4660
- style: styles14.input
5114
+ style: {
5115
+ ...styles14.input,
5116
+ backgroundColor: pal.inputBg,
5117
+ borderColor: pal.inputBorder,
5118
+ color: pal.inputText
5119
+ }
4661
5120
  }
4662
5121
  ),
4663
5122
  unreadCount > 0 && /* @__PURE__ */ jsx("span", { style: styles14.unreadBadge, title: `${unreadCount} unread`, children: unreadCount > 99 ? "99+" : unreadCount }),
@@ -4677,7 +5136,11 @@ function InlineInputBar({
4677
5136
  "button",
4678
5137
  {
4679
5138
  onClick: onExpand,
4680
- style: styles14.expandButton,
5139
+ style: {
5140
+ ...styles14.expandButton,
5141
+ borderColor: pal.expandBorder,
5142
+ color: pal.expandColor
5143
+ },
4681
5144
  title: "Expand to full chat",
4682
5145
  type: "button",
4683
5146
  children: "\u26F6"
@@ -4946,6 +5409,10 @@ function ConversationListItem({
4946
5409
  }) {
4947
5410
  const hasUnread = item.unreadCount > 0;
4948
5411
  const displayName = item.name || item.patientSnapshot?.patientName || "Unknown";
5412
+ const studyType = item.patientSnapshot?.studyType;
5413
+ const patientId = item.patientSnapshot?.patientId;
5414
+ const showStudyRow = !!(studyType || patientId);
5415
+ const initials = computeInitials2(item.patientSnapshot?.patientName || displayName);
4949
5416
  return /* @__PURE__ */ jsxs(
4950
5417
  "button",
4951
5418
  {
@@ -4953,15 +5420,11 @@ function ConversationListItem({
4953
5420
  onClick,
4954
5421
  style: {
4955
5422
  ...styles15.container,
4956
- ...isSelected ? styles15.selected : {},
4957
- ...hasUnread ? styles15.unread : {}
5423
+ ...isSelected ? styles15.selected : {}
4958
5424
  },
4959
5425
  type: "button",
4960
5426
  children: [
4961
- /* @__PURE__ */ jsxs("div", { style: styles15.avatarWrapper, children: [
4962
- item.picture ? /* @__PURE__ */ jsx("img", { src: item.picture, alt: "", style: styles15.avatar }) : /* @__PURE__ */ jsx("div", { style: styles15.avatarFallback, children: (displayName).charAt(0).toUpperCase() }),
4963
- hasUnread && /* @__PURE__ */ jsx("span", { style: styles15.unreadDot })
4964
- ] }),
5427
+ /* @__PURE__ */ jsx("div", { style: styles15.avatarWrapper, children: item.picture ? /* @__PURE__ */ jsx("img", { src: item.picture, alt: "", style: styles15.avatarImg }) : /* @__PURE__ */ jsx("div", { style: styles15.avatarTile, "aria-hidden": "true", children: initials }) }),
4965
5428
  /* @__PURE__ */ jsxs("div", { style: styles15.content, children: [
4966
5429
  /* @__PURE__ */ jsxs("div", { style: styles15.topRow, children: [
4967
5430
  /* @__PURE__ */ jsx(
@@ -4985,101 +5448,120 @@ function ConversationListItem({
4985
5448
  }
4986
5449
  )
4987
5450
  ] }),
5451
+ showStudyRow && /* @__PURE__ */ jsxs("div", { style: styles15.studyRow, children: [
5452
+ studyType && /* @__PURE__ */ jsx("span", { style: styles15.studyType, children: studyType }),
5453
+ studyType && patientId && /* @__PURE__ */ jsx("span", { style: styles15.studyDivider, children: "\xB7" }),
5454
+ patientId && /* @__PURE__ */ jsxs("span", { style: styles15.studyId, children: [
5455
+ "MRN ",
5456
+ patientId
5457
+ ] })
5458
+ ] }),
4988
5459
  /* @__PURE__ */ jsxs("div", { style: styles15.bottomRow, children: [
4989
- /* @__PURE__ */ jsx(
4990
- "span",
4991
- {
4992
- style: {
4993
- ...styles15.preview,
4994
- ...hasUnread ? styles15.previewUnread : {}
4995
- },
4996
- children: renderLastMessagePreview(item.lastMessage)
4997
- }
4998
- ),
4999
- hasUnread && /* @__PURE__ */ jsx("span", { style: styles15.badge, children: item.unreadCount > 99 ? "99+" : item.unreadCount })
5460
+ /* @__PURE__ */ jsx(PreviewLine, { message: item.lastMessage, hasUnread }),
5461
+ hasUnread && /* @__PURE__ */ jsx("span", { style: styles15.badge, "aria-label": `${item.unreadCount} unread`, children: item.unreadCount > 99 ? "99+" : item.unreadCount })
5000
5462
  ] })
5001
5463
  ] })
5002
5464
  ]
5003
5465
  }
5004
5466
  );
5005
5467
  }
5006
- function renderLastMessagePreview(message) {
5007
- if (!message) return "No messages yet";
5008
- const prefix = message.senderName ? `${message.senderName}: ` : "";
5468
+ function PreviewLine({ message, hasUnread }) {
5469
+ if (!message) {
5470
+ return /* @__PURE__ */ jsx("span", { style: styles15.preview, children: "No messages yet" });
5471
+ }
5472
+ if (message.type === "system") {
5473
+ return /* @__PURE__ */ jsx("span", { style: styles15.preview, children: message.body });
5474
+ }
5475
+ const text = previewText(message);
5476
+ const sender = message.senderName ? `${message.senderName.split(" ")[0]}:` : "";
5477
+ return /* @__PURE__ */ jsxs(
5478
+ "span",
5479
+ {
5480
+ style: {
5481
+ ...styles15.preview,
5482
+ ...hasUnread ? styles15.previewUnread : {}
5483
+ },
5484
+ children: [
5485
+ sender && /* @__PURE__ */ jsxs("span", { style: styles15.previewSender, children: [
5486
+ sender,
5487
+ " "
5488
+ ] }),
5489
+ text
5490
+ ]
5491
+ }
5492
+ );
5493
+ }
5494
+ function previewText(message) {
5009
5495
  switch (message.type) {
5010
5496
  case "audio":
5011
- return `${prefix}\u{1F3A4} Voice message`;
5497
+ return "Voice message";
5012
5498
  case "image":
5013
- return `${prefix}\u{1F4F7} ${message.fileName || "Image"}`;
5499
+ return message.fileName || "Image";
5014
5500
  case "file":
5015
- return `${prefix}\u{1F4CE} ${message.fileName || "File"}`;
5501
+ return message.fileName || "File";
5016
5502
  case "deep_link":
5017
- return `${prefix}\u{1F517} ${message.body}`;
5018
- case "system":
5019
- return message.body;
5503
+ return message.body || "Shared link";
5020
5504
  default: {
5021
5505
  const body = message.body || "";
5022
- const preview = body.length > 60 ? `${body.slice(0, 60)}\u2026` : body;
5023
- return `${prefix}${preview}`;
5506
+ return body.length > 60 ? `${body.slice(0, 60)}\u2026` : body;
5024
5507
  }
5025
5508
  }
5026
5509
  }
5510
+ function computeInitials2(name) {
5511
+ return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("") || "?";
5512
+ }
5027
5513
  var styles15 = {
5028
5514
  container: {
5029
5515
  display: "flex",
5030
- alignItems: "center",
5031
- gap: SPACE.S3,
5032
- padding: `${SPACE.S3} ${SPACE.S3}`,
5516
+ alignItems: "flex-start",
5517
+ gap: "14px",
5033
5518
  width: "100%",
5519
+ padding: `14px ${SPACE.S5}`,
5520
+ minHeight: "84px",
5034
5521
  backgroundColor: "transparent",
5035
5522
  border: "none",
5036
- borderBottom: `1px solid ${COLOR.neutral100}`,
5523
+ borderLeft: "4px solid transparent",
5037
5524
  cursor: "pointer",
5038
- textAlign: "left"
5525
+ textAlign: "left",
5526
+ color: "inherit",
5527
+ fontFamily: "inherit",
5528
+ transition: "background-color 120ms ease"
5039
5529
  },
5040
5530
  selected: {
5041
- backgroundColor: COLOR.primaryBg
5531
+ backgroundColor: COLOR.primaryBg,
5532
+ borderLeftColor: COLOR.primary
5042
5533
  },
5043
- unread: {},
5044
5534
  avatarWrapper: {
5045
- position: "relative",
5046
- width: SIZE.avatar,
5047
- height: SIZE.avatar,
5535
+ width: "52px",
5536
+ height: "52px",
5048
5537
  flexShrink: 0
5049
5538
  },
5050
- avatar: {
5051
- width: SIZE.avatar,
5052
- height: SIZE.avatar,
5053
- borderRadius: RADIUS.full,
5054
- objectFit: "cover"
5539
+ avatarImg: {
5540
+ width: "52px",
5541
+ height: "52px",
5542
+ borderRadius: RADIUS.xl,
5543
+ objectFit: "cover",
5544
+ boxShadow: "inset 0 -2px 0 rgba(0, 0, 0, 0.08)"
5055
5545
  },
5056
- avatarFallback: {
5057
- width: SIZE.avatar,
5058
- height: SIZE.avatar,
5059
- borderRadius: RADIUS.full,
5060
- backgroundColor: COLOR.neutral200,
5546
+ avatarTile: {
5547
+ width: "52px",
5548
+ height: "52px",
5549
+ borderRadius: RADIUS.xl,
5550
+ backgroundColor: COLOR.primary,
5551
+ color: COLOR.white,
5061
5552
  display: "flex",
5062
5553
  alignItems: "center",
5063
5554
  justifyContent: "center",
5064
- fontSize: FONT_SIZE.md,
5065
- fontWeight: FONT_WEIGHT.semibold,
5066
- color: COLOR.neutral500
5067
- },
5068
- unreadDot: {
5069
- position: "absolute",
5070
- top: "0",
5071
- left: "0",
5072
- width: SIZE.dot,
5073
- height: SIZE.dot,
5074
- borderRadius: RADIUS.full,
5075
- backgroundColor: COLOR.primary,
5076
- border: `2px solid ${COLOR.white}`
5555
+ fontSize: FONT_SIZE.lg,
5556
+ fontWeight: FONT_WEIGHT.bold,
5557
+ letterSpacing: "0.02em",
5558
+ boxShadow: "inset 0 -2px 0 rgba(0, 0, 0, 0.08)"
5077
5559
  },
5078
5560
  content: {
5079
5561
  flex: 1,
5080
5562
  display: "flex",
5081
5563
  flexDirection: "column",
5082
- gap: "2px",
5564
+ gap: "3px",
5083
5565
  minWidth: 0
5084
5566
  },
5085
5567
  topRow: {
@@ -5104,123 +5586,194 @@ var styles15 = {
5104
5586
  time: {
5105
5587
  fontSize: FONT_SIZE.xs,
5106
5588
  color: COLOR.neutral500,
5589
+ fontVariantNumeric: "tabular-nums",
5107
5590
  flexShrink: 0
5108
5591
  },
5109
5592
  timeUnread: {
5110
5593
  color: COLOR.primary,
5111
5594
  fontWeight: FONT_WEIGHT.semibold
5112
5595
  },
5596
+ studyRow: {
5597
+ display: "flex",
5598
+ alignItems: "center",
5599
+ gap: SPACE.S2,
5600
+ minWidth: 0
5601
+ },
5602
+ studyType: {
5603
+ fontSize: FONT_SIZE.sm,
5604
+ color: COLOR.neutral500,
5605
+ overflow: "hidden",
5606
+ textOverflow: "ellipsis",
5607
+ whiteSpace: "nowrap"
5608
+ },
5609
+ studyDivider: {
5610
+ fontSize: FONT_SIZE.sm,
5611
+ color: COLOR.neutral400,
5612
+ flexShrink: 0
5613
+ },
5614
+ studyId: {
5615
+ fontSize: FONT_SIZE.sm,
5616
+ color: COLOR.neutral500,
5617
+ fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
5618
+ fontVariantNumeric: "tabular-nums",
5619
+ flexShrink: 0
5620
+ },
5113
5621
  bottomRow: {
5114
5622
  display: "flex",
5115
5623
  justifyContent: "space-between",
5116
5624
  alignItems: "center",
5117
- gap: SPACE.S2
5625
+ gap: SPACE.S2,
5626
+ marginTop: "2px"
5118
5627
  },
5119
5628
  preview: {
5120
5629
  fontSize: FONT_SIZE.sm,
5121
- color: COLOR.neutral600,
5630
+ color: COLOR.neutral500,
5122
5631
  overflow: "hidden",
5123
5632
  textOverflow: "ellipsis",
5124
5633
  whiteSpace: "nowrap",
5125
5634
  flex: 1,
5126
5635
  minWidth: 0
5127
5636
  },
5637
+ previewSender: {
5638
+ color: COLOR.neutral700,
5639
+ fontWeight: FONT_WEIGHT.semibold
5640
+ },
5128
5641
  previewUnread: {
5129
5642
  color: COLOR.neutral900,
5130
5643
  fontWeight: FONT_WEIGHT.medium
5131
5644
  },
5132
5645
  badge: {
5133
- minWidth: "20px",
5134
- height: "20px",
5135
- padding: `0 ${SPACE.S2}`,
5646
+ minWidth: "22px",
5647
+ height: "22px",
5648
+ padding: "0 9px",
5136
5649
  fontSize: FONT_SIZE.xs,
5137
- fontWeight: FONT_WEIGHT.semibold,
5650
+ fontWeight: FONT_WEIGHT.bold,
5138
5651
  color: COLOR.white,
5139
- backgroundColor: COLOR.primary,
5652
+ backgroundColor: COLOR.danger,
5140
5653
  borderRadius: RADIUS.pill,
5141
5654
  display: "flex",
5142
5655
  alignItems: "center",
5143
5656
  justifyContent: "center",
5657
+ fontVariantNumeric: "tabular-nums",
5144
5658
  flexShrink: 0
5145
5659
  }
5146
5660
  };
5661
+ function SearchIcon({ size = 20, color = "currentColor" }) {
5662
+ return /* @__PURE__ */ jsxs(
5663
+ "svg",
5664
+ {
5665
+ xmlns: "http://www.w3.org/2000/svg",
5666
+ width: size,
5667
+ height: size,
5668
+ viewBox: "0 0 24 24",
5669
+ fill: "none",
5670
+ stroke: color,
5671
+ strokeWidth: "2",
5672
+ strokeLinecap: "round",
5673
+ strokeLinejoin: "round",
5674
+ "aria-hidden": "true",
5675
+ children: [
5676
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
5677
+ /* @__PURE__ */ jsx("path", { d: "M20 20l-4-4" })
5678
+ ]
5679
+ }
5680
+ );
5681
+ }
5147
5682
  function ConversationList({
5148
5683
  conversations,
5149
5684
  selectedId,
5150
5685
  isLoading,
5151
5686
  error,
5152
5687
  onSelect,
5153
- title = "Conversations",
5154
5688
  className
5155
5689
  }) {
5690
+ const { totalUnread } = useCollab();
5156
5691
  const [query, setQuery] = useState("");
5157
- const [showUnreadOnly, setShowUnreadOnly] = useState(false);
5692
+ const [filter, setFilter] = useState(
5693
+ () => totalUnread > 0 ? "unread" : "all"
5694
+ );
5695
+ const [searchFocused, setSearchFocused] = useState(false);
5696
+ const unreadCount = useMemo(
5697
+ () => conversations.filter((c) => c.unreadCount > 0).length,
5698
+ [conversations]
5699
+ );
5700
+ const autoDefaultedRef = useRef(false);
5701
+ useEffect(() => {
5702
+ if (autoDefaultedRef.current) return;
5703
+ if (conversations.length === 0) return;
5704
+ autoDefaultedRef.current = true;
5705
+ if (unreadCount > 0) setFilter("unread");
5706
+ }, [conversations.length, unreadCount]);
5158
5707
  const filtered = useMemo(() => {
5159
5708
  let result = conversations;
5160
- if (showUnreadOnly) {
5709
+ if (filter === "unread") {
5161
5710
  result = result.filter((c) => c.unreadCount > 0);
5162
5711
  }
5163
5712
  const q = query.trim().toLowerCase();
5164
5713
  if (q) {
5165
5714
  result = result.filter((c) => {
5166
- const patient = c.patientSnapshot?.patientName?.toLowerCase() || "";
5167
5715
  const name = c.name.toLowerCase();
5168
- return name.includes(q) || patient.includes(q);
5716
+ const patient = c.patientSnapshot?.patientName?.toLowerCase() || "";
5717
+ const studyType = c.patientSnapshot?.studyType?.toLowerCase() || "";
5718
+ const patientId = c.patientSnapshot?.patientId?.toLowerCase() || "";
5719
+ return name.includes(q) || patient.includes(q) || studyType.includes(q) || patientId.includes(q);
5169
5720
  });
5170
5721
  }
5171
5722
  return result;
5172
- }, [conversations, query, showUnreadOnly]);
5173
- const totalUnread = conversations.reduce((sum, c) => sum + c.unreadCount, 0);
5723
+ }, [conversations, query, filter]);
5174
5724
  return /* @__PURE__ */ jsxs("div", { className, style: styles16.container, children: [
5175
- /* @__PURE__ */ jsxs("div", { style: styles16.header, children: [
5176
- /* @__PURE__ */ jsxs("div", { style: styles16.titleRow, children: [
5177
- /* @__PURE__ */ jsx("h3", { style: styles16.title, children: title }),
5178
- totalUnread > 0 && /* @__PURE__ */ jsx("span", { style: styles16.totalBadge, children: totalUnread })
5179
- ] }),
5180
- /* @__PURE__ */ jsx("div", { style: styles16.searchRow, children: /* @__PURE__ */ jsx(
5181
- "input",
5725
+ /* @__PURE__ */ jsxs("div", { style: styles16.tabs, children: [
5726
+ /* @__PURE__ */ jsx(
5727
+ TabButton,
5182
5728
  {
5183
- type: "text",
5184
- value: query,
5185
- onChange: (e) => setQuery(e.target.value),
5186
- placeholder: "Search patients or channels...",
5187
- style: styles16.searchInput
5729
+ label: "Unread",
5730
+ count: unreadCount,
5731
+ isActive: filter === "unread",
5732
+ onClick: () => setFilter("unread")
5188
5733
  }
5189
- ) }),
5190
- /* @__PURE__ */ jsxs("div", { style: styles16.filterRow, children: [
5191
- /* @__PURE__ */ jsx(
5192
- "button",
5193
- {
5194
- onClick: () => setShowUnreadOnly(false),
5195
- style: {
5196
- ...styles16.filterButton,
5197
- ...!showUnreadOnly ? styles16.filterButtonActive : {}
5198
- },
5199
- type: "button",
5200
- children: "All"
5201
- }
5202
- ),
5203
- /* @__PURE__ */ jsxs(
5204
- "button",
5205
- {
5206
- onClick: () => setShowUnreadOnly(true),
5207
- style: {
5208
- ...styles16.filterButton,
5209
- ...showUnreadOnly ? styles16.filterButtonActive : {}
5210
- },
5211
- type: "button",
5212
- children: [
5213
- "Unread ",
5214
- totalUnread > 0 && `(${totalUnread})`
5215
- ]
5216
- }
5217
- )
5218
- ] })
5734
+ ),
5735
+ /* @__PURE__ */ jsx(
5736
+ TabButton,
5737
+ {
5738
+ label: "All",
5739
+ count: conversations.length,
5740
+ isActive: filter === "all",
5741
+ onClick: () => setFilter("all")
5742
+ }
5743
+ )
5219
5744
  ] }),
5745
+ /* @__PURE__ */ jsxs(
5746
+ "div",
5747
+ {
5748
+ style: {
5749
+ ...styles16.search,
5750
+ ...searchFocused ? styles16.searchFocused : {}
5751
+ },
5752
+ children: [
5753
+ /* @__PURE__ */ jsx("span", { style: styles16.searchIcon, "aria-hidden": "true", children: /* @__PURE__ */ jsx(SearchIcon, { size: 20, color: COLOR.neutral500 }) }),
5754
+ /* @__PURE__ */ jsx(
5755
+ "input",
5756
+ {
5757
+ type: "text",
5758
+ value: query,
5759
+ onChange: (e) => setQuery(e.target.value),
5760
+ onFocus: () => setSearchFocused(true),
5761
+ onBlur: () => setSearchFocused(false),
5762
+ placeholder: "Search by patient name or MRN",
5763
+ "aria-label": "Search conversations",
5764
+ style: styles16.searchInput
5765
+ }
5766
+ )
5767
+ ]
5768
+ }
5769
+ ),
5220
5770
  /* @__PURE__ */ jsxs("div", { style: styles16.list, children: [
5221
- isLoading && /* @__PURE__ */ jsx("div", { style: styles16.state, children: "Loading conversations..." }),
5222
- error && /* @__PURE__ */ jsx("div", { style: styles16.errorState, children: error }),
5223
- !isLoading && !error && filtered.length === 0 && /* @__PURE__ */ jsx("div", { style: styles16.state, children: query ? "No matching conversations" : "No conversations yet" }),
5771
+ isLoading && /* @__PURE__ */ jsxs("div", { style: styles16.loading, children: [
5772
+ /* @__PURE__ */ jsx(Spinner, { size: 16 }),
5773
+ /* @__PURE__ */ jsx("span", { children: "Loading conversations\u2026" })
5774
+ ] }),
5775
+ error && !isLoading && /* @__PURE__ */ jsx("div", { style: styles16.errorState, children: error }),
5776
+ !isLoading && !error && filtered.length === 0 && /* @__PURE__ */ jsx(EmptyState, { filter, hasQuery: query.trim().length > 0 }),
5224
5777
  filtered.map((item) => /* @__PURE__ */ jsx(
5225
5778
  ConversationListItem,
5226
5779
  {
@@ -5233,95 +5786,178 @@ function ConversationList({
5233
5786
  ] })
5234
5787
  ] });
5235
5788
  }
5789
+ function TabButton({
5790
+ label,
5791
+ count,
5792
+ isActive,
5793
+ onClick
5794
+ }) {
5795
+ return /* @__PURE__ */ jsxs(
5796
+ "button",
5797
+ {
5798
+ type: "button",
5799
+ onClick,
5800
+ style: {
5801
+ ...styles16.tab,
5802
+ ...isActive ? styles16.tabActive : {}
5803
+ },
5804
+ children: [
5805
+ /* @__PURE__ */ jsx("span", { children: label }),
5806
+ /* @__PURE__ */ jsx(
5807
+ "span",
5808
+ {
5809
+ style: {
5810
+ ...styles16.tabCount,
5811
+ ...isActive ? styles16.tabCountActive : {}
5812
+ },
5813
+ children: count
5814
+ }
5815
+ )
5816
+ ]
5817
+ }
5818
+ );
5819
+ }
5820
+ function EmptyState({ filter, hasQuery }) {
5821
+ let title = "No conversations yet";
5822
+ let subtitle = "New chats will show up here as cases come in.";
5823
+ if (filter === "unread") {
5824
+ title = "No unread messages";
5825
+ subtitle = "You're all caught up.";
5826
+ } else if (hasQuery) {
5827
+ title = "No conversations match";
5828
+ subtitle = "Try a different search.";
5829
+ }
5830
+ return /* @__PURE__ */ jsxs("div", { style: styles16.empty, children: [
5831
+ /* @__PURE__ */ jsx(ChatIllustration, { size: 88 }),
5832
+ /* @__PURE__ */ jsx("p", { style: styles16.emptyTitle, children: title }),
5833
+ /* @__PURE__ */ jsx("p", { style: styles16.emptySubtitle, children: subtitle })
5834
+ ] });
5835
+ }
5236
5836
  var styles16 = {
5237
5837
  container: {
5238
5838
  display: "flex",
5239
5839
  flexDirection: "column",
5240
5840
  height: "100%",
5241
- backgroundColor: COLOR.white,
5242
- borderRight: `1px solid ${COLOR.neutral200}`
5841
+ backgroundColor: COLOR.white
5243
5842
  },
5244
- header: {
5245
- padding: SPACE.S3,
5843
+ // Tabs sit at the top of the panel now (header section removed).
5844
+ // A bit more top padding so they don't crowd the top edge.
5845
+ tabs: {
5846
+ display: "flex",
5847
+ gap: SPACE.S2,
5848
+ padding: `${SPACE.S5} ${SPACE.S5} ${SPACE.S2}`,
5246
5849
  borderBottom: `1px solid ${COLOR.neutral200}`,
5247
- backgroundColor: COLOR.neutral50,
5248
5850
  flexShrink: 0
5249
5851
  },
5250
- titleRow: {
5251
- display: "flex",
5852
+ tab: {
5853
+ display: "inline-flex",
5252
5854
  alignItems: "center",
5253
5855
  gap: SPACE.S2,
5254
- marginBottom: SPACE.S2
5255
- },
5256
- title: {
5257
- margin: 0,
5258
- fontSize: FONT_SIZE.md,
5856
+ padding: `${SPACE.S2} ${SPACE.S4}`,
5857
+ backgroundColor: "transparent",
5858
+ border: "none",
5859
+ borderRadius: RADIUS.lg,
5860
+ cursor: "pointer",
5861
+ color: COLOR.neutral500,
5862
+ fontFamily: "inherit",
5863
+ fontSize: FONT_SIZE.sm,
5259
5864
  fontWeight: FONT_WEIGHT.semibold,
5260
- color: COLOR.neutral900
5865
+ transition: "background-color 120ms ease, color 120ms ease"
5261
5866
  },
5262
- totalBadge: {
5263
- minWidth: "20px",
5264
- height: "20px",
5265
- padding: `0 ${SPACE.S2}`,
5867
+ tabActive: {
5868
+ backgroundColor: COLOR.primaryBg,
5869
+ color: COLOR.primary
5870
+ },
5871
+ tabCount: {
5266
5872
  fontSize: FONT_SIZE.xs,
5267
- fontWeight: FONT_WEIGHT.semibold,
5268
- color: COLOR.white,
5269
- backgroundColor: COLOR.primary,
5873
+ fontVariantNumeric: "tabular-nums",
5874
+ padding: "1px 8px",
5270
5875
  borderRadius: RADIUS.pill,
5876
+ backgroundColor: COLOR.neutral200,
5877
+ color: COLOR.neutral500,
5878
+ fontWeight: FONT_WEIGHT.semibold
5879
+ },
5880
+ tabCountActive: {
5881
+ backgroundColor: COLOR.primary,
5882
+ color: COLOR.white
5883
+ },
5884
+ search: {
5271
5885
  display: "flex",
5272
5886
  alignItems: "center",
5273
- justifyContent: "center"
5274
- },
5275
- searchRow: {
5276
- marginBottom: SPACE.S2
5887
+ gap: SPACE.S2,
5888
+ margin: `14px ${SPACE.S5} ${SPACE.S3}`,
5889
+ padding: `${SPACE.S3} 14px`,
5890
+ backgroundColor: COLOR.neutral100,
5891
+ border: `1.5px solid ${COLOR.neutral200}`,
5892
+ borderRadius: "14px",
5893
+ transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease"
5277
5894
  },
5278
- searchInput: {
5279
- width: "100%",
5280
- padding: `${SPACE.S2} ${SPACE.S3}`,
5281
- fontSize: FONT_SIZE.sm,
5282
- border: `1px solid ${COLOR.neutral200}`,
5283
- borderRadius: RADIUS.md,
5284
- outline: "none",
5285
- fontFamily: "inherit"
5895
+ searchFocused: {
5896
+ backgroundColor: COLOR.white,
5897
+ borderColor: COLOR.primary,
5898
+ boxShadow: `0 0 0 3px ${COLOR.primaryBg}`
5286
5899
  },
5287
- filterRow: {
5900
+ searchIcon: {
5288
5901
  display: "flex",
5289
- gap: SPACE.S1
5902
+ alignItems: "center",
5903
+ justifyContent: "center",
5904
+ flexShrink: 0
5290
5905
  },
5291
- filterButton: {
5906
+ searchInput: {
5292
5907
  flex: 1,
5293
- padding: `${SPACE.S1} ${SPACE.S2}`,
5294
- fontSize: FONT_SIZE.xs,
5295
- color: COLOR.neutral500,
5296
- backgroundColor: COLOR.white,
5297
- border: `1px solid ${COLOR.neutral200}`,
5298
- borderRadius: RADIUS.md,
5299
- cursor: "pointer"
5300
- },
5301
- filterButtonActive: {
5302
- color: COLOR.white,
5303
- backgroundColor: COLOR.primary,
5304
- borderColor: COLOR.primary,
5305
- fontWeight: FONT_WEIGHT.medium
5908
+ minWidth: 0,
5909
+ border: "none",
5910
+ background: "transparent",
5911
+ outline: "none",
5912
+ fontFamily: "inherit",
5913
+ fontSize: FONT_SIZE.md,
5914
+ color: COLOR.neutral900
5306
5915
  },
5307
5916
  list: {
5308
5917
  flex: 1,
5309
- overflowY: "auto"
5918
+ overflowY: "auto",
5919
+ padding: "4px 0 12px"
5310
5920
  },
5311
- state: {
5312
- padding: `${SPACE.S6} ${SPACE.S4}`,
5921
+ loading: {
5922
+ display: "flex",
5923
+ alignItems: "center",
5924
+ justifyContent: "center",
5925
+ gap: SPACE.S2,
5926
+ padding: `44px ${SPACE.S5}`,
5313
5927
  fontSize: FONT_SIZE.sm,
5314
- color: COLOR.neutral500,
5315
- textAlign: "center"
5928
+ color: COLOR.neutral500
5316
5929
  },
5317
5930
  errorState: {
5318
5931
  padding: SPACE.S4,
5932
+ margin: SPACE.S3,
5319
5933
  fontSize: FONT_SIZE.sm,
5320
5934
  color: COLOR.danger,
5321
5935
  backgroundColor: COLOR.dangerBg,
5322
- margin: SPACE.S3,
5936
+ border: `1px solid ${COLOR.dangerBorder}`,
5323
5937
  borderRadius: RADIUS.md,
5324
5938
  textAlign: "center"
5939
+ },
5940
+ empty: {
5941
+ display: "flex",
5942
+ flexDirection: "column",
5943
+ alignItems: "center",
5944
+ justifyContent: "center",
5945
+ padding: "44px 24px",
5946
+ textAlign: "center",
5947
+ color: COLOR.neutral500
5948
+ },
5949
+ emptyTitle: {
5950
+ margin: "12px 0 4px",
5951
+ fontSize: FONT_SIZE.md,
5952
+ fontWeight: FONT_WEIGHT.semibold,
5953
+ color: COLOR.neutral700
5954
+ },
5955
+ emptySubtitle: {
5956
+ margin: 0,
5957
+ fontSize: FONT_SIZE.sm,
5958
+ color: COLOR.neutral500,
5959
+ maxWidth: "280px",
5960
+ lineHeight: LINE_HEIGHT.normal
5325
5961
  }
5326
5962
  };
5327
5963
  ensureGlobalStyles();
@@ -5329,7 +5965,6 @@ var COMPACT_BREAKPOINT = 720;
5329
5965
  function CollabInbox({
5330
5966
  initialConversationId,
5331
5967
  onSelectConversation,
5332
- title,
5333
5968
  className,
5334
5969
  style
5335
5970
  }) {
@@ -5366,8 +6001,7 @@ function CollabInbox({
5366
6001
  selectedId,
5367
6002
  isLoading,
5368
6003
  error,
5369
- onSelect: handleSelect,
5370
- title
6004
+ onSelect: handleSelect
5371
6005
  }
5372
6006
  ) }),
5373
6007
  showDetail && /* @__PURE__ */ jsx("div", { style: isCompact ? styles17.mainCompact : styles17.main, children: selected ? /* @__PURE__ */ jsx(
@@ -5380,7 +6014,7 @@ function CollabInbox({
5380
6014
  onBack: isCompact ? () => setSelectedId(null) : void 0
5381
6015
  },
5382
6016
  selected.id
5383
- ) : /* @__PURE__ */ jsx(EmptyState, { hasAny: conversations.length > 0 }) })
6017
+ ) : /* @__PURE__ */ jsx(EmptyState2, { hasAny: conversations.length > 0 }) })
5384
6018
  ]
5385
6019
  }
5386
6020
  );
@@ -5414,7 +6048,7 @@ function parseChannelName(name, orderId) {
5414
6048
  orderId
5415
6049
  };
5416
6050
  }
5417
- function EmptyState({ hasAny }) {
6051
+ function EmptyState2({ hasAny }) {
5418
6052
  return /* @__PURE__ */ jsxs("div", { style: styles17.empty, children: [
5419
6053
  /* @__PURE__ */ jsx("div", { style: styles17.emptyIcon, children: "\u{1F4AC}" }),
5420
6054
  /* @__PURE__ */ jsx("h3", { style: styles17.emptyTitle, children: hasAny ? "Select a conversation" : "No conversations yet" }),
@@ -5793,6 +6427,6 @@ function usePinnedMessages({
5793
6427
  };
5794
6428
  }
5795
6429
 
5796
- export { AUDIO_MIME_TYPE, ChannelSettings, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, ConversationList, ConversationListItem, DEEP_LINK_PREFIX, EVENTS, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, MessageActionsMenu, MessageBubble, MessageInput, MessageList, ParticipantsList, PatientHeader, PinnedMessagesBar, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, TYPING_DEBOUNCE_MS, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
6430
+ export { AUDIO_MIME_TYPE, ChannelSettings, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, ConversationList, ConversationListItem, DEEP_LINK_PREFIX, EVENTS, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, MessageActionsMenu, MessageBubble, MessageInput, MessageList, ParticipantsList, PatientHeader, PinnedMessagesBar, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, THEME_DEFAULTS, THEME_VAR, TYPING_DEBOUNCE_MS, applyThemeOverrides, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
5797
6431
  //# sourceMappingURL=index.mjs.map
5798
6432
  //# sourceMappingURL=index.mjs.map