@natoe/colab 0.1.13 → 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
@@ -15,6 +15,17 @@ declare const THEME_VAR: {
15
15
  readonly dangerBorder: "--natoe-colab-danger-border";
16
16
  readonly dangerFg: "--natoe-colab-danger-fg";
17
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";
18
29
  };
19
30
  /** Default values for the themeable tokens (used by both CSS emission and
20
31
  * the inline-style fallback inside `var(..., fallback)`). */
@@ -30,6 +41,17 @@ declare const THEME_DEFAULTS: {
30
41
  readonly dangerBorder: "#fecaca";
31
42
  readonly dangerFg: "#b91c1c";
32
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";
33
55
  };
34
56
  /** Public override shape — passed via CollabConfig.theme. Every key is
35
57
  * optional; unspecified keys fall back to {@link THEME_DEFAULTS}. */
@@ -241,6 +263,14 @@ interface CollabPanelProps {
241
263
  * relying on potentially-incomplete patientData.
242
264
  */
243
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';
244
274
  /** Custom class name for the outer container */
245
275
  className?: string;
246
276
  /** Custom inline styles for the outer container */
@@ -250,7 +280,7 @@ interface CollabPanelProps {
250
280
  * Full collaboration panel: patient header + pinned bar + message thread + input.
251
281
  * Supports reply, pin/unpin, and (optionally) seen-by indicators.
252
282
  */
253
- 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;
254
284
 
255
285
  interface CollabPopupProps {
256
286
  /** Order ID for this conversation */
@@ -290,6 +320,7 @@ interface CollabPopupProps {
290
320
  */
291
321
  declare function CollabPopup({ orderId, patientData, participantIds, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
292
322
 
323
+ type ColabInlineMode = 'light' | 'dark';
293
324
  interface CollabInlineProps {
294
325
  orderId: string;
295
326
  patientData: PatientData;
@@ -297,24 +328,32 @@ interface CollabInlineProps {
297
328
  participantIds?: string[];
298
329
  /** Called when user clicks the expand button — host opens the floating popup */
299
330
  onExpand?: () => void;
300
- /** Max inline messages to show (default 5) */
331
+ /** Max inline messages to show (default 1 — just the latest) */
301
332
  messageLimit?: number;
302
333
  /** Placeholder text for the input */
303
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;
304
342
  className?: string;
305
343
  style?: React.CSSProperties;
306
344
  }
307
345
  /**
308
- * Inline chat for table rows.
346
+ * Inline chat for table rows or side panels.
309
347
  *
310
348
  * Two states:
311
349
  * - No conversation yet: just an input bar (no socket, no preview)
312
- * - Conversation exists: last N messages + input + expand button
350
+ * - Conversation exists: latest message (or last N if messageLimit > 1)
351
+ * + input + expand button
313
352
  *
314
353
  * Supports text and voice messages. File attachments and full chat features
315
354
  * stay in the floating popup / inbox.
316
355
  */
317
- 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;
318
357
 
319
358
  interface CollabInboxProps {
320
359
  /** Optional: preselect a specific conversation (by conversation ID) */
@@ -358,12 +397,31 @@ interface ConversationCallbacks {
358
397
  messageId: string;
359
398
  }) => void;
360
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
+ }
361
412
  /**
362
413
  * Manages Phoenix WebSocket connection and channel subscriptions.
363
414
  * One instance per authenticated user session.
364
415
  */
365
416
  declare class CollabSocket {
366
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
+ */
367
425
  private channels;
368
426
  private presences;
369
427
  private userChannel;
@@ -377,10 +435,30 @@ declare class CollabSocket {
377
435
  private joinUserChannel;
378
436
  /** Register callback for unread count changes */
379
437
  onUnreadCountUpdate(callback: (counts: Record<string, number>) => void): void;
380
- /** Join a conversation channel and subscribe to events */
381
- joinConversation(conversationId: string, callbacks: ConversationCallbacks): Channel | null;
382
- /** Leave a conversation channel */
383
- 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;
384
462
  /** Send a message to a conversation.
385
463
  *
386
464
  * Phoenix buffers pushes fired while a channel is still joining and
@@ -716,8 +794,13 @@ interface ConversationListProps {
716
794
  /**
717
795
  * Scrollable inbox of conversations. Unread/All tabs (with count pills)
718
796
  * sit at the top, followed by a search field with a focus-within ring.
719
- * On first data load with any unread items the inbox auto-defaults to
720
- * the Unread tab so triage starts on what matters. Empty/loading/error
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
721
804
  * states are illustrated and descriptive rather than terse one-liners.
722
805
  */
723
806
  declare function ConversationList({ conversations, selectedId, isLoading, error, onSelect, className, }: ConversationListProps): react_jsx_runtime.JSX.Element;
@@ -774,7 +857,7 @@ interface PatientHeaderProps {
774
857
  * experience and there's no outer title, so we still show the name +
775
858
  * back button stacked above the case card.
776
859
  */
777
- declare function PatientHeader({ patientData, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
860
+ declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
778
861
 
779
862
  interface MessageListHandle {
780
863
  /** Scroll to a specific message by ID (used by pin jump-to) */
package/dist/index.d.ts CHANGED
@@ -15,6 +15,17 @@ declare const THEME_VAR: {
15
15
  readonly dangerBorder: "--natoe-colab-danger-border";
16
16
  readonly dangerFg: "--natoe-colab-danger-fg";
17
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";
18
29
  };
19
30
  /** Default values for the themeable tokens (used by both CSS emission and
20
31
  * the inline-style fallback inside `var(..., fallback)`). */
@@ -30,6 +41,17 @@ declare const THEME_DEFAULTS: {
30
41
  readonly dangerBorder: "#fecaca";
31
42
  readonly dangerFg: "#b91c1c";
32
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";
33
55
  };
34
56
  /** Public override shape — passed via CollabConfig.theme. Every key is
35
57
  * optional; unspecified keys fall back to {@link THEME_DEFAULTS}. */
@@ -241,6 +263,14 @@ interface CollabPanelProps {
241
263
  * relying on potentially-incomplete patientData.
242
264
  */
243
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';
244
274
  /** Custom class name for the outer container */
245
275
  className?: string;
246
276
  /** Custom inline styles for the outer container */
@@ -250,7 +280,7 @@ interface CollabPanelProps {
250
280
  * Full collaboration panel: patient header + pinned bar + message thread + input.
251
281
  * Supports reply, pin/unpin, and (optionally) seen-by indicators.
252
282
  */
253
- 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;
254
284
 
255
285
  interface CollabPopupProps {
256
286
  /** Order ID for this conversation */
@@ -290,6 +320,7 @@ interface CollabPopupProps {
290
320
  */
291
321
  declare function CollabPopup({ orderId, patientData, participantIds, isOpen, onClose, onBack, onMinimize, initialPosition, width, height, className, }: CollabPopupProps): react_jsx_runtime.JSX.Element | null;
292
322
 
323
+ type ColabInlineMode = 'light' | 'dark';
293
324
  interface CollabInlineProps {
294
325
  orderId: string;
295
326
  patientData: PatientData;
@@ -297,24 +328,32 @@ interface CollabInlineProps {
297
328
  participantIds?: string[];
298
329
  /** Called when user clicks the expand button — host opens the floating popup */
299
330
  onExpand?: () => void;
300
- /** Max inline messages to show (default 5) */
331
+ /** Max inline messages to show (default 1 — just the latest) */
301
332
  messageLimit?: number;
302
333
  /** Placeholder text for the input */
303
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;
304
342
  className?: string;
305
343
  style?: React.CSSProperties;
306
344
  }
307
345
  /**
308
- * Inline chat for table rows.
346
+ * Inline chat for table rows or side panels.
309
347
  *
310
348
  * Two states:
311
349
  * - No conversation yet: just an input bar (no socket, no preview)
312
- * - Conversation exists: last N messages + input + expand button
350
+ * - Conversation exists: latest message (or last N if messageLimit > 1)
351
+ * + input + expand button
313
352
  *
314
353
  * Supports text and voice messages. File attachments and full chat features
315
354
  * stay in the floating popup / inbox.
316
355
  */
317
- 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;
318
357
 
319
358
  interface CollabInboxProps {
320
359
  /** Optional: preselect a specific conversation (by conversation ID) */
@@ -358,12 +397,31 @@ interface ConversationCallbacks {
358
397
  messageId: string;
359
398
  }) => void;
360
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
+ }
361
412
  /**
362
413
  * Manages Phoenix WebSocket connection and channel subscriptions.
363
414
  * One instance per authenticated user session.
364
415
  */
365
416
  declare class CollabSocket {
366
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
+ */
367
425
  private channels;
368
426
  private presences;
369
427
  private userChannel;
@@ -377,10 +435,30 @@ declare class CollabSocket {
377
435
  private joinUserChannel;
378
436
  /** Register callback for unread count changes */
379
437
  onUnreadCountUpdate(callback: (counts: Record<string, number>) => void): void;
380
- /** Join a conversation channel and subscribe to events */
381
- joinConversation(conversationId: string, callbacks: ConversationCallbacks): Channel | null;
382
- /** Leave a conversation channel */
383
- 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;
384
462
  /** Send a message to a conversation.
385
463
  *
386
464
  * Phoenix buffers pushes fired while a channel is still joining and
@@ -716,8 +794,13 @@ interface ConversationListProps {
716
794
  /**
717
795
  * Scrollable inbox of conversations. Unread/All tabs (with count pills)
718
796
  * sit at the top, followed by a search field with a focus-within ring.
719
- * On first data load with any unread items the inbox auto-defaults to
720
- * the Unread tab so triage starts on what matters. Empty/loading/error
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
721
804
  * states are illustrated and descriptive rather than terse one-liners.
722
805
  */
723
806
  declare function ConversationList({ conversations, selectedId, isLoading, error, onSelect, className, }: ConversationListProps): react_jsx_runtime.JSX.Element;
@@ -774,7 +857,7 @@ interface PatientHeaderProps {
774
857
  * experience and there's no outer title, so we still show the name +
775
858
  * back button stacked above the case card.
776
859
  */
777
- declare function PatientHeader({ patientData, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
860
+ declare function PatientHeader({ patientData, participants, onOpenDicom, onOpenSettings, onBack, hideName, displayName, className, }: PatientHeaderProps): react_jsx_runtime.JSX.Element;
778
861
 
779
862
  interface MessageListHandle {
780
863
  /** Scroll to a specific message by ID (used by pin jump-to) */