@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.d.mts CHANGED
@@ -2,6 +2,61 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React from 'react';
3
3
  import { Channel } from 'phoenix';
4
4
 
5
+ /** CSS variable names for the themeable color tokens. */
6
+ declare const THEME_VAR: {
7
+ readonly primary: "--natoe-colab-primary";
8
+ readonly primaryHover: "--natoe-colab-primary-hover";
9
+ readonly primaryBg: "--natoe-colab-primary-bg";
10
+ readonly primaryFg: "--natoe-colab-primary-fg";
11
+ readonly success: "--natoe-colab-success";
12
+ readonly warning: "--natoe-colab-warning";
13
+ readonly danger: "--natoe-colab-danger";
14
+ readonly dangerBg: "--natoe-colab-danger-bg";
15
+ readonly dangerBorder: "--natoe-colab-danger-border";
16
+ readonly dangerFg: "--natoe-colab-danger-fg";
17
+ readonly fontStack: "--natoe-colab-font-stack";
18
+ readonly white: "--natoe-colab-white";
19
+ readonly neutral50: "--natoe-colab-neutral-50";
20
+ readonly neutral100: "--natoe-colab-neutral-100";
21
+ readonly neutral200: "--natoe-colab-neutral-200";
22
+ readonly neutral300: "--natoe-colab-neutral-300";
23
+ readonly neutral400: "--natoe-colab-neutral-400";
24
+ readonly neutral500: "--natoe-colab-neutral-500";
25
+ readonly neutral600: "--natoe-colab-neutral-600";
26
+ readonly neutral700: "--natoe-colab-neutral-700";
27
+ readonly neutral800: "--natoe-colab-neutral-800";
28
+ readonly neutral900: "--natoe-colab-neutral-900";
29
+ };
30
+ /** Default values for the themeable tokens (used by both CSS emission and
31
+ * the inline-style fallback inside `var(..., fallback)`). */
32
+ declare const THEME_DEFAULTS: {
33
+ readonly primary: "#2563eb";
34
+ readonly primaryHover: "#1d4ed8";
35
+ readonly primaryBg: "#dbeafe";
36
+ readonly primaryFg: "#1d4ed8";
37
+ readonly success: "#059669";
38
+ readonly warning: "#d97706";
39
+ readonly danger: "#dc2626";
40
+ readonly dangerBg: "#fef2f2";
41
+ readonly dangerBorder: "#fecaca";
42
+ readonly dangerFg: "#b91c1c";
43
+ readonly fontStack: "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif";
44
+ readonly white: "#ffffff";
45
+ readonly neutral50: "#f9fafb";
46
+ readonly neutral100: "#f3f4f6";
47
+ readonly neutral200: "#e5e7eb";
48
+ readonly neutral300: "#d1d5db";
49
+ readonly neutral400: "#9ca3af";
50
+ readonly neutral500: "#6b7280";
51
+ readonly neutral600: "#4b5563";
52
+ readonly neutral700: "#374151";
53
+ readonly neutral800: "#1f2937";
54
+ readonly neutral900: "#111827";
55
+ };
56
+ /** Public override shape — passed via CollabConfig.theme. Every key is
57
+ * optional; unspecified keys fall back to {@link THEME_DEFAULTS}. */
58
+ type Theme = Partial<Record<keyof typeof THEME_DEFAULTS, string>>;
59
+
5
60
  interface CollabConfig {
6
61
  /** Phoenix WebSocket URL (e.g. wss://backend.natoe.ai/socket) */
7
62
  socketUrl: string;
@@ -12,6 +67,15 @@ interface CollabConfig {
12
67
  userRole: UserRole;
13
68
  userName: string;
14
69
  userAvatar?: string;
70
+ /**
71
+ * Optional theme overrides. Any key set here is applied as a CSS custom
72
+ * property on `:root`, replacing the package's default for that token.
73
+ * Keys not provided keep their default. See {@link Theme} for the
74
+ * complete list of overridable tokens (brand + semantic colors + font
75
+ * stack). Spacing, type scale, radii are deliberately not themeable —
76
+ * changing those wholesale breaks the package's visual rhythm.
77
+ */
78
+ theme?: Theme;
15
79
  /** Host app callbacks — colab calls these, never implements them */
16
80
  onOpenDicom?: (studyId: string, storageId: string) => void;
17
81
  onUploadFile?: (file: File | Blob, fileName?: string) => Promise<string>;
@@ -23,6 +87,8 @@ interface PatientData {
23
87
  patientName: string;
24
88
  patientAge?: string;
25
89
  patientSex?: string;
90
+ /** Patient MRN — shown on inbox rows and searchable in the inbox filter. */
91
+ patientId?: string;
26
92
  studyType?: string;
27
93
  studyId?: string;
28
94
  storageId?: string;
@@ -145,8 +211,13 @@ interface SendMessagePayload {
145
211
  media_duration?: number;
146
212
  file_name?: string;
147
213
  metadata?: Record<string, unknown>;
148
- /** ID of message being replied to (snapshot is built server-side) */
149
- replyToId?: string;
214
+ /**
215
+ * ID of message being replied to (snapshot is built server-side).
216
+ * Wire-level key is snake_case to match the channel handler — the
217
+ * other optional fields here (media_url, file_name, …) follow the
218
+ * same convention.
219
+ */
220
+ reply_to_id?: string;
150
221
  }
151
222
  type ChannelEvent = 'message:new' | 'message:read' | 'message:pinned' | 'message:unpinned' | 'user:typing' | 'user:joined' | 'user:left' | 'channel:updated' | 'channel:deleted';
152
223
  interface TypingEvent {
@@ -192,6 +263,14 @@ interface CollabPanelProps {
192
263
  * relying on potentially-incomplete patientData.
193
264
  */
194
265
  onConversationChange?: (conversation: Conversation | null) => void;
266
+ /**
267
+ * Color mode for the entire panel. Default 'light'. 'dark' is meant for
268
+ * embedding inside the viewer's left panel (#000 background) — applies
269
+ * dark overrides for the themeable CSS variables on the panel root, so
270
+ * every internal surface (PatientHeader, MessageList, MessageBubble,
271
+ * MessageInput, PinnedMessagesBar, etc.) inherits them via cascade.
272
+ */
273
+ themeMode?: 'light' | 'dark';
195
274
  /** Custom class name for the outer container */
196
275
  className?: string;
197
276
  /** Custom inline styles for the outer container */
@@ -201,7 +280,7 @@ interface CollabPanelProps {
201
280
  * Full collaboration panel: patient header + pinned bar + message thread + input.
202
281
  * Supports reply, pin/unpin, and (optionally) seen-by indicators.
203
282
  */
204
- declare function CollabPanel({ orderId, patientData, participantIds, showSeenBy, onBack, hidePatientName, onConversationChange, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
283
+ declare function CollabPanel({ orderId, patientData, participantIds, showSeenBy, onBack, hidePatientName, onConversationChange, themeMode, className, style, }: CollabPanelProps): react_jsx_runtime.JSX.Element;
205
284
 
206
285
  interface CollabPopupProps {
207
286
  /** Order ID for this conversation */
@@ -216,6 +295,14 @@ interface CollabPopupProps {
216
295
  onClose: () => void;
217
296
  /** Optional back navigation — renders a ← button in the title bar when provided */
218
297
  onBack?: () => void;
298
+ /**
299
+ * When provided, the minimize button calls this callback instead of
300
+ * toggling the popup's own shrink-to-titlebar state. Hosts use this
301
+ * to minimize the popup to an external chip / dock. With this set the
302
+ * minimize button always shows the minimize glyph (no expand state),
303
+ * since the host owns the open/minimized lifecycle.
304
+ */
305
+ onMinimize?: () => void;
219
306
  /** Initial position (defaults to bottom-right) */
220
307
  initialPosition?: {
221
308
  x: number;
@@ -231,8 +318,9 @@ interface CollabPopupProps {
231
318
  * Draggable floating chat popup — drop-in replacement for DraggableChatPopup.
232
319
  * Renders a CollabPanel inside a movable, resizable container.
233
320
  */
234
- declare function CollabPopup({ orderId, patientData, participantIds, isOpen, onClose, onBack, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
321
+ declare function CollabPopup({ orderId, patientData, participantIds, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
235
322
 
323
+ type ColabInlineMode = 'light' | 'dark';
236
324
  interface CollabInlineProps {
237
325
  orderId: string;
238
326
  patientData: PatientData;
@@ -240,32 +328,38 @@ interface CollabInlineProps {
240
328
  participantIds?: string[];
241
329
  /** Called when user clicks the expand button — host opens the floating popup */
242
330
  onExpand?: () => void;
243
- /** Max inline messages to show (default 5) */
331
+ /** Max inline messages to show (default 1 — just the latest) */
244
332
  messageLimit?: number;
245
333
  /** Placeholder text for the input */
246
334
  placeholder?: string;
335
+ /**
336
+ * Color mode for the inline surface. Defaults to 'light'. 'dark' is meant
337
+ * for embedding inside the viewer's left panel (black background) — only
338
+ * neutral/structural colors switch; brand/role/semantic colors stay the
339
+ * same in both modes.
340
+ */
341
+ mode?: ColabInlineMode;
247
342
  className?: string;
248
343
  style?: React.CSSProperties;
249
344
  }
250
345
  /**
251
- * Inline chat for table rows.
346
+ * Inline chat for table rows or side panels.
252
347
  *
253
348
  * Two states:
254
349
  * - No conversation yet: just an input bar (no socket, no preview)
255
- * - Conversation exists: last N messages + input + expand button
350
+ * - Conversation exists: latest message (or last N if messageLimit > 1)
351
+ * + input + expand button
256
352
  *
257
353
  * Supports text and voice messages. File attachments and full chat features
258
354
  * stay in the floating popup / inbox.
259
355
  */
260
- declare function CollabInline({ orderId, patientData, participantIds, onExpand, messageLimit, placeholder, className, style, }: CollabInlineProps): react_jsx_runtime.JSX.Element;
356
+ declare function CollabInline({ orderId, patientData, participantIds, onExpand, messageLimit, placeholder, mode, className, style, }: CollabInlineProps): react_jsx_runtime.JSX.Element;
261
357
 
262
358
  interface CollabInboxProps {
263
359
  /** Optional: preselect a specific conversation (by conversation ID) */
264
360
  initialConversationId?: string;
265
361
  /** Optional callback when the user selects a conversation */
266
362
  onSelectConversation?: (item: ConversationListItem$1) => void;
267
- /** Title for the sidebar */
268
- title?: string;
269
363
  className?: string;
270
364
  style?: React.CSSProperties;
271
365
  }
@@ -276,7 +370,7 @@ interface CollabInboxProps {
276
370
  * Uses the same CollabPanel as the floating popup, so all features
277
371
  * (reply, pin, seen-by, DICOM, voice, attachments) are available.
278
372
  */
279
- declare function CollabInbox({ initialConversationId, onSelectConversation, title, className, style, }: CollabInboxProps): react_jsx_runtime.JSX.Element;
373
+ declare function CollabInbox({ initialConversationId, onSelectConversation, className, style, }: CollabInboxProps): react_jsx_runtime.JSX.Element;
280
374
 
281
375
  type MessageCallback = (message: Message) => void;
282
376
  type TypingCallback = (event: TypingEvent) => void;
@@ -303,12 +397,31 @@ interface ConversationCallbacks {
303
397
  messageId: string;
304
398
  }) => void;
305
399
  }
400
+ /**
401
+ * Returned by `joinConversation()`. Each caller gets its own handle and
402
+ * its own bound listeners — calling `release()` unbinds only THIS
403
+ * subscriber's listeners. The channel only actually leaves the server
404
+ * when the last subscriber releases.
405
+ */
406
+ interface ChannelSubscription {
407
+ /** Underlying Phoenix channel — callers that need to push/observe directly. */
408
+ channel: Channel;
409
+ /** Drop this subscriber. Idempotent — safe to call twice. */
410
+ release: () => void;
411
+ }
306
412
  /**
307
413
  * Manages Phoenix WebSocket connection and channel subscriptions.
308
414
  * One instance per authenticated user session.
309
415
  */
310
416
  declare class CollabSocket {
311
417
  private socket;
418
+ /**
419
+ * Conversation channels keyed by id. Each entry is reference-counted so
420
+ * multiple surfaces (e.g. inline chat + expanded panel mounted at once
421
+ * for the same conversation) can coexist without one's `leaveConversation`
422
+ * tearing the channel out from under the other. See `joinConversation`
423
+ * and the returned `ChannelSubscription.release`.
424
+ */
312
425
  private channels;
313
426
  private presences;
314
427
  private userChannel;
@@ -322,10 +435,30 @@ declare class CollabSocket {
322
435
  private joinUserChannel;
323
436
  /** Register callback for unread count changes */
324
437
  onUnreadCountUpdate(callback: (counts: Record<string, number>) => void): void;
325
- /** Join a conversation channel and subscribe to events */
326
- joinConversation(conversationId: string, callbacks: ConversationCallbacks): Channel | null;
327
- /** Leave a conversation channel */
328
- leaveConversation(conversationId: string): void;
438
+ /**
439
+ * Join a conversation channel and subscribe to events.
440
+ *
441
+ * Reference-counted: multiple callers can join the same conversation
442
+ * (e.g. inline preview + expanded panel mounted side-by-side). Each
443
+ * call binds its own listeners and gets back a `ChannelSubscription`.
444
+ * The underlying channel only `.leave()`s the server when the LAST
445
+ * subscriber calls `release()`.
446
+ */
447
+ joinConversation(conversationId: string, callbacks: ConversationCallbacks): ChannelSubscription | null;
448
+ /**
449
+ * @deprecated Use the `release()` method returned by `joinConversation()`.
450
+ * Kept as a no-op so older callers don't throw — but it cannot identify
451
+ * which subscriber should leave, so it silently does nothing. Any code
452
+ * still calling this will leak listeners and prevent the channel from
453
+ * ever being torn down. Migrate to the subscription handle.
454
+ */
455
+ leaveConversation(_conversationId: string): void;
456
+ /**
457
+ * Look up the underlying Phoenix Channel for a conversation, if any
458
+ * subscriber is still holding it. All send/push paths go through this
459
+ * helper so the refcounted entry shape is contained to joinConversation.
460
+ */
461
+ private getChannel;
329
462
  /** Send a message to a conversation.
330
463
  *
331
464
  * Phoenix buffers pushes fired while a channel is still joining and
@@ -372,6 +505,13 @@ interface CollabContextValue {
372
505
  config: CollabConfig;
373
506
  apiBaseUrl: string;
374
507
  totalUnread: number;
508
+ /**
509
+ * Per-conversation unread counts, keyed by conversation ID. Updated
510
+ * live via the user_notifications channel's unread_update push, so
511
+ * surfaces like a minimized-chat chip can reflect changes without
512
+ * mounting their own useConversation.
513
+ */
514
+ unreadCounts: Record<string, number>;
375
515
  /** Batched preview lookup — coalesces calls within a microtask */
376
516
  requestPreview: (orderId: string) => Promise<ConversationPreview | null>;
377
517
  /** Invalidate a cached preview (e.g. when a new message arrives) */
@@ -649,14 +789,21 @@ interface ConversationListProps {
649
789
  isLoading: boolean;
650
790
  error: string | null;
651
791
  onSelect: (item: ConversationListItem$1) => void;
652
- /** Optional title shown above the list */
653
- title?: string;
654
792
  className?: string;
655
793
  }
656
794
  /**
657
- * Scrollable sidebar list of conversations with search and unread filter.
795
+ * Scrollable inbox of conversations. Unread/All tabs (with count pills)
796
+ * sit at the top, followed by a search field with a focus-within ring.
797
+ * The inbox lands on Unread by default whenever there are any unread
798
+ * conversations — the initial choice reads CollabProvider's socket-
799
+ * pushed `totalUnread`, which is already populated by the time the
800
+ * FAB is clicked (it drives the badge), so there's no "All flash"
801
+ * during the conversation-list fetch. A fallback effect handles the
802
+ * edge case where the conversation list loads with unread items but
803
+ * `totalUnread` was still zero at mount time. Empty/loading/error
804
+ * states are illustrated and descriptive rather than terse one-liners.
658
805
  */
659
- declare function ConversationList({ conversations, selectedId, isLoading, error, onSelect, title, className, }: ConversationListProps): react_jsx_runtime.JSX.Element;
806
+ declare function ConversationList({ conversations, selectedId, isLoading, error, onSelect, className, }: ConversationListProps): react_jsx_runtime.JSX.Element;
660
807
 
661
808
  interface ConversationListItemProps {
662
809
  item: ConversationListItem$1;
@@ -665,8 +812,21 @@ interface ConversationListItemProps {
665
812
  className?: string;
666
813
  }
667
814
  /**
668
- * Single row in the inbox sidebar — shows patient/channel name, last message
669
- * preview, timestamp, and unread badge.
815
+ * Single row in the inbox sidebar.
816
+ *
817
+ * Three-row layout (top-to-bottom):
818
+ * 1. Patient/channel name + relative time
819
+ * 2. Study type + monospace `MRN <patientId>` (surfaced from the
820
+ * snapshot; either field is optional and the row collapses if both
821
+ * are missing)
822
+ * 3. Last message preview (sender prefix in stronger weight) + unread
823
+ * count badge on the right
824
+ *
825
+ * Active row gets a 4px brand-color left-border accent + tinted background.
826
+ * The avatar is a 52px **square** tile (16px rounded corners) with brand-
827
+ * coloured fill and white initials — easier to scan than a circle at this
828
+ * size and consistent across the inbox without depending on any case
829
+ * status field we don't model.
670
830
  */
671
831
  declare function ConversationListItem({ item, isSelected, onClick, className, }: ConversationListItemProps): react_jsx_runtime.JSX.Element;
672
832
 
@@ -683,7 +843,21 @@ interface PatientHeaderProps {
683
843
  displayName?: string;
684
844
  className?: string;
685
845
  }
686
- declare function PatientHeader({ patientData, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
846
+ /**
847
+ * "Case card" header that sits below the window title bar. Tinted blue-50
848
+ * background to set it apart from the conversation body. Renders a row
849
+ * of labelled key-value columns (label uppercase on top, value below) for
850
+ * the clinical context the host passed in, with the action buttons
851
+ * (View DICOM + Settings) right-aligned.
852
+ *
853
+ * Used in two layouts:
854
+ * - Popup path (`hideName=true`): the surrounding title bar already
855
+ * shows the patient name, so we render the case card only.
856
+ * - Compact-inbox path (`hideName=false`): the panel is a full-page
857
+ * experience and there's no outer title, so we still show the name +
858
+ * back button stacked above the case card.
859
+ */
860
+ declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
687
861
 
688
862
  interface MessageListHandle {
689
863
  /** Scroll to a specific message by ID (used by pin jump-to) */
@@ -864,6 +1038,34 @@ interface ParticipantsListProps {
864
1038
  }
865
1039
  declare function ParticipantsList({ participants, currentUserId, isAdmin, onRemoveUser, className, }: ParticipantsListProps): react_jsx_runtime.JSX.Element;
866
1040
 
1041
+ /**
1042
+ * Shared style tokens + the global stylesheet the package injects once per
1043
+ * page.
1044
+ *
1045
+ * Top-level wrappers (CollabPopup, CollabPanel, CollabInline, CollabInbox)
1046
+ * import {@link FONT_STACK} so every surface renders with the same font
1047
+ * stack, even when the package is mounted inside an iframe whose
1048
+ * surrounding document doesn't set one.
1049
+ *
1050
+ * Themeable tokens (brand + semantic colors + the font stack) are emitted
1051
+ * as CSS custom properties on `:root` so host apps can override them via
1052
+ * `CollabConfig.theme` (see {@link applyThemeOverrides}). Non-themeable
1053
+ * tokens (neutrals, slate, spacing, radius, sizes) live as inline values
1054
+ * in core/theme.ts and stay constant.
1055
+ *
1056
+ * The prefers-contrast / prefers-reduced-motion adjustments are emitted
1057
+ * inside the same stylesheet so they apply wherever a `natoe-colab-root`
1058
+ * wrapper is mounted.
1059
+ */
1060
+
1061
+ /**
1062
+ * Apply a host-supplied theme by setting CSS custom properties on the
1063
+ * document root. Keys missing from `theme` have their previously-set
1064
+ * override cleared so toggling overrides back to undefined returns the
1065
+ * default — never leaves stale values behind.
1066
+ */
1067
+ declare function applyThemeOverrides(theme: Theme | undefined): void;
1068
+
867
1069
  /** Phoenix channel event names — must match backend channel implementation */
868
1070
  declare const EVENTS: {
869
1071
  readonly MESSAGE_NEW: "message:new";
@@ -902,4 +1104,4 @@ declare const SUPPORTED_IMAGE_TYPES: string[];
902
1104
  /** Max file size in bytes (20MB) */
903
1105
  declare const MAX_FILE_SIZE: number;
904
1106
 
905
- export { AUDIO_MIME_TYPE, type ChannelEvent, ChannelSettings, type ChannelUpdatePayload, type CollabConfig, type CollabError, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, type Conversation, ConversationList, ConversationListItem, type ConversationListItem$1 as ConversationListItemData, type ConversationPreview, type CreateConversationResponse, DEEP_LINK_PREFIX, EVENTS, type InviteUserPayload, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, type Message, MessageActionsMenu, MessageBubble, MessageInput, MessageList, type MessageListHandle, type MessageSnapshot, type MessageType, type Participant, ParticipantsList, type PatientData, PatientHeader, PinnedMessagesBar, type PreviewBatchResponse, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, type SendMessagePayload, TYPING_DEBOUNCE_MS, type TypingEvent, type UserRole, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };
1107
+ export { AUDIO_MIME_TYPE, type ChannelEvent, ChannelSettings, type ChannelUpdatePayload, type CollabConfig, type CollabError, CollabInbox, CollabInline, CollabPanel, CollabPopup, CollabProvider, CollabSocket, type Conversation, ConversationList, ConversationListItem, type ConversationListItem$1 as ConversationListItemData, type ConversationPreview, type CreateConversationResponse, DEEP_LINK_PREFIX, EVENTS, type InviteUserPayload, MAX_FILE_SIZE, MAX_PINNED_MESSAGES, MESSAGES_PAGE_SIZE, MESSAGE_TYPES, type Message, MessageActionsMenu, MessageBubble, MessageInput, MessageList, type MessageListHandle, type MessageSnapshot, type MessageType, type Participant, ParticipantsList, type PatientData, PatientHeader, PinnedMessagesBar, type PreviewBatchResponse, ReplyPreview, ReplyQuoteBlock, SUPPORTED_IMAGE_TYPES, SeenByIndicator, type SendMessagePayload, THEME_DEFAULTS, THEME_VAR, TYPING_DEBOUNCE_MS, type Theme, type TypingEvent, type UserRole, applyThemeOverrides, useAudioRecorder, useChannelSettings, useCollab, useConversation, useConversationList, useDeepLinks, useInlineCollab, useMessages, usePinnedMessages, useUnreadCount };