@natoe/colab 0.1.5 → 0.1.7

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,6 +1,7 @@
1
- import React4, { createContext, forwardRef, useRef, useImperativeHandle, useEffect, useCallback, useState, useContext, useId, useMemo } from 'react';
1
+ import React4, { createContext, forwardRef, useRef, useImperativeHandle, useEffect, useCallback, useState, useLayoutEffect, useContext, useId, useMemo } from 'react';
2
2
  import { Socket, Presence } from 'phoenix';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+ import { createPortal } from 'react-dom';
4
5
 
5
6
  // src/components/CollabPanel.tsx
6
7
 
@@ -370,7 +371,9 @@ function CollabProvider({ config, apiBaseUrl, children }) {
370
371
  const data = snakeToCamel(await response.json());
371
372
  orderIds.forEach((orderId) => {
372
373
  const preview = data.previews[orderId] ?? null;
373
- previewCache.current.set(orderId, preview);
374
+ if (preview !== null) {
375
+ previewCache.current.set(orderId, preview);
376
+ }
374
377
  resolvers.get(orderId)?.forEach((resolve) => resolve(preview));
375
378
  });
376
379
  } catch (error) {
@@ -417,7 +420,8 @@ function CollabProvider({ config, apiBaseUrl, children }) {
417
420
  if (!response.ok) {
418
421
  throw { code: "FETCH_ERROR", message: "Failed to fetch messages", status: response.status };
419
422
  }
420
- return snakeToCamel(await response.json());
423
+ const msgs = snakeToCamel(await response.json());
424
+ return msgs.sort((a, b) => a.insertedAt.localeCompare(b.insertedAt));
421
425
  },
422
426
  [apiBaseUrl, authHeaders]
423
427
  );
@@ -891,8 +895,8 @@ function useConversation({
891
895
  const payload = {
892
896
  body: "Voice message",
893
897
  type: "audio",
894
- mediaUrl: url,
895
- mediaDuration: duration
898
+ media_url: url,
899
+ media_duration: duration
896
900
  };
897
901
  const { conv, persistedByCreate } = await ensureConversation(payload);
898
902
  if (persistedByCreate) return;
@@ -908,8 +912,8 @@ function useConversation({
908
912
  const payload = {
909
913
  body: file.name,
910
914
  type: isImage ? "image" : "file",
911
- mediaUrl: url,
912
- fileName: file.name
915
+ media_url: url,
916
+ file_name: file.name
913
917
  };
914
918
  const { conv, persistedByCreate } = await ensureConversation(payload);
915
919
  if (persistedByCreate) return;
@@ -1141,71 +1145,190 @@ function useDeepLinks() {
1141
1145
  function escapeRegex(string) {
1142
1146
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1143
1147
  }
1148
+ function DicomIcon({ size = 18, color = "currentColor" }) {
1149
+ return /* @__PURE__ */ jsxs(
1150
+ "svg",
1151
+ {
1152
+ xmlns: "http://www.w3.org/2000/svg",
1153
+ width: size,
1154
+ height: size,
1155
+ viewBox: "0 0 24 24",
1156
+ fill: "none",
1157
+ stroke: color,
1158
+ strokeWidth: "2",
1159
+ strokeLinecap: "round",
1160
+ strokeLinejoin: "round",
1161
+ "aria-hidden": "true",
1162
+ children: [
1163
+ /* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "13", height: "13", rx: "2" }),
1164
+ /* @__PURE__ */ jsx("path", { d: "M7 7h13a2 2 0 0 1 2 2v11", opacity: "0.6" }),
1165
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9.5", r: "1.2", fill: color, stroke: "none" }),
1166
+ /* @__PURE__ */ jsx("path", { d: "M5 14l3-3 4 4 2-2 3 3" })
1167
+ ]
1168
+ }
1169
+ );
1170
+ }
1171
+ function SettingsIcon({ size = 18, color = "currentColor" }) {
1172
+ return /* @__PURE__ */ jsx(
1173
+ "svg",
1174
+ {
1175
+ xmlns: "http://www.w3.org/2000/svg",
1176
+ width: size,
1177
+ height: size,
1178
+ viewBox: "0 0 24 24",
1179
+ fill: color,
1180
+ "aria-hidden": "true",
1181
+ 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" })
1182
+ }
1183
+ );
1184
+ }
1185
+ function BackIcon({ size = 22, color = "currentColor" }) {
1186
+ return /* @__PURE__ */ jsx(
1187
+ "svg",
1188
+ {
1189
+ xmlns: "http://www.w3.org/2000/svg",
1190
+ width: size,
1191
+ height: size,
1192
+ viewBox: "0 0 24 24",
1193
+ fill: color,
1194
+ "aria-hidden": "true",
1195
+ children: /* @__PURE__ */ jsx("path", { d: "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" })
1196
+ }
1197
+ );
1198
+ }
1144
1199
  function PatientHeader({
1145
1200
  patientData,
1146
1201
  onOpenDicom,
1147
1202
  onOpenSettings,
1203
+ onBack,
1148
1204
  className
1149
1205
  }) {
1150
1206
  const hasDicom = !!(patientData.studyId && patientData.storageId);
1207
+ const meta = [];
1208
+ if (patientData.patientAge) meta.push({ label: "Age", value: String(patientData.patientAge) });
1209
+ if (patientData.patientSex) meta.push({ label: "Sex", value: String(patientData.patientSex) });
1210
+ if (patientData.studyType) meta.push({ label: "Modality", value: patientData.studyType });
1211
+ if (patientData.bodyParts && patientData.bodyParts.length > 0) {
1212
+ meta.push({ label: "Body part", value: patientData.bodyParts.join(", ") });
1213
+ }
1214
+ if (patientData.referringPhysician) {
1215
+ meta.push({ label: "Ref", value: patientData.referringPhysician });
1216
+ }
1151
1217
  return /* @__PURE__ */ jsxs("div", { className, style: styles.container, children: [
1152
- /* @__PURE__ */ jsx("div", { style: styles.infoRow, children: /* @__PURE__ */ jsx("div", { style: styles.patientInfo, children: /* @__PURE__ */ jsx("span", { style: styles.patientName, children: buildChannelName(patientData) }) }) }),
1153
- /* @__PURE__ */ jsxs("div", { style: styles.secondaryRow, children: [
1154
- patientData.patientAge && /* @__PURE__ */ jsxs("span", { style: styles.secondary, children: [
1155
- patientData.patientAge,
1156
- patientData.patientSex ? `/${patientData.patientSex}` : ""
1157
- ] }),
1158
- patientData.studyType && /* @__PURE__ */ jsx("span", { style: styles.secondary, children: patientData.studyType }),
1159
- patientData.bodyParts && patientData.bodyParts.length > 0 && /* @__PURE__ */ jsx("span", { style: styles.secondary, children: patientData.bodyParts.join(", ") }),
1160
- patientData.referringPhysician && /* @__PURE__ */ jsxs("span", { style: styles.secondary, children: [
1161
- "Ref: ",
1162
- patientData.referringPhysician
1163
- ] })
1218
+ /* @__PURE__ */ jsxs("div", { style: styles.nameRow, children: [
1219
+ onBack && /* @__PURE__ */ jsx(
1220
+ "button",
1221
+ {
1222
+ type: "button",
1223
+ onClick: onBack,
1224
+ style: styles.backButton,
1225
+ "aria-label": "Back to conversations",
1226
+ title: "Back to conversations",
1227
+ children: /* @__PURE__ */ jsx(BackIcon, { size: 22, color: "#1f2937" })
1228
+ }
1229
+ ),
1230
+ /* @__PURE__ */ jsx("span", { style: styles.nameText, children: buildChannelName(patientData) })
1164
1231
  ] }),
1165
- /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
1166
- hasDicom && onOpenDicom && /* @__PURE__ */ jsx("button", { onClick: onOpenDicom, style: styles.dicomButton, type: "button", children: "View DICOM" }),
1167
- onOpenSettings && /* @__PURE__ */ jsx("button", { onClick: onOpenSettings, style: styles.settingsButton, type: "button", children: "Settings" })
1168
- ] })
1232
+ meta.length > 0 && /* @__PURE__ */ jsx("div", { style: styles.metaRow, children: meta.map((item) => /* @__PURE__ */ jsxs("span", { style: styles.metaItem, children: [
1233
+ /* @__PURE__ */ jsxs("span", { style: styles.metaLabel, children: [
1234
+ item.label,
1235
+ ":"
1236
+ ] }),
1237
+ " ",
1238
+ /* @__PURE__ */ jsx("span", { style: styles.metaValue, children: item.value })
1239
+ ] }, item.label)) }),
1240
+ hasDicom && onOpenDicom || onOpenSettings ? /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
1241
+ hasDicom && onOpenDicom && /* @__PURE__ */ jsxs(
1242
+ "button",
1243
+ {
1244
+ onClick: onOpenDicom,
1245
+ style: styles.dicomButton,
1246
+ type: "button",
1247
+ "aria-label": "View DICOM study",
1248
+ children: [
1249
+ /* @__PURE__ */ jsx(DicomIcon, { size: 18, color: "#ffffff" }),
1250
+ /* @__PURE__ */ jsx("span", { children: "View DICOM" })
1251
+ ]
1252
+ }
1253
+ ),
1254
+ onOpenSettings && /* @__PURE__ */ jsxs(
1255
+ "button",
1256
+ {
1257
+ onClick: onOpenSettings,
1258
+ style: styles.settingsButton,
1259
+ type: "button",
1260
+ "aria-label": "Open channel settings",
1261
+ children: [
1262
+ /* @__PURE__ */ jsx(SettingsIcon, { size: 18, color: "#374151" }),
1263
+ /* @__PURE__ */ jsx("span", { children: "Settings" })
1264
+ ]
1265
+ }
1266
+ )
1267
+ ] }) : null
1169
1268
  ] });
1170
1269
  }
1171
1270
  var styles = {
1172
1271
  container: {
1173
- padding: "12px 16px",
1272
+ padding: "14px 16px",
1174
1273
  borderBottom: "1px solid #e5e7eb",
1175
1274
  backgroundColor: "#f9fafb"
1176
1275
  },
1177
- infoRow: {
1276
+ nameRow: {
1178
1277
  display: "flex",
1179
- justifyContent: "space-between",
1180
- alignItems: "center"
1278
+ alignItems: "center",
1279
+ gap: "6px"
1181
1280
  },
1182
- patientInfo: {
1281
+ backButton: {
1282
+ width: "36px",
1283
+ height: "36px",
1183
1284
  display: "flex",
1184
1285
  alignItems: "center",
1185
- gap: "8px",
1186
- flexWrap: "wrap"
1286
+ justifyContent: "center",
1287
+ marginLeft: "-8px",
1288
+ backgroundColor: "transparent",
1289
+ border: "none",
1290
+ borderRadius: "50%",
1291
+ cursor: "pointer",
1292
+ flexShrink: 0
1187
1293
  },
1188
- patientName: {
1294
+ nameText: {
1295
+ fontSize: "18px",
1189
1296
  fontWeight: 600,
1190
- fontSize: "17px",
1191
- color: "#111827"
1297
+ color: "#111827",
1298
+ lineHeight: 1.3,
1299
+ minWidth: 0,
1300
+ overflow: "hidden",
1301
+ textOverflow: "ellipsis",
1302
+ whiteSpace: "nowrap"
1192
1303
  },
1193
- secondaryRow: {
1304
+ metaRow: {
1194
1305
  display: "flex",
1195
- gap: "12px",
1196
- marginTop: "4px",
1197
- alignItems: "center"
1306
+ flexWrap: "wrap",
1307
+ columnGap: "14px",
1308
+ rowGap: "4px",
1309
+ marginTop: "6px"
1198
1310
  },
1199
- secondary: {
1311
+ metaItem: {
1200
1312
  fontSize: "13px",
1201
- color: "#4b5563"
1313
+ color: "#374151",
1314
+ whiteSpace: "nowrap"
1315
+ },
1316
+ metaLabel: {
1317
+ color: "#6b7280",
1318
+ fontWeight: 500
1319
+ },
1320
+ metaValue: {
1321
+ color: "#111827"
1202
1322
  },
1203
1323
  actions: {
1204
1324
  display: "flex",
1205
1325
  gap: "8px",
1206
- marginTop: "8px"
1326
+ marginTop: "10px"
1207
1327
  },
1208
1328
  dicomButton: {
1329
+ display: "inline-flex",
1330
+ alignItems: "center",
1331
+ gap: "8px",
1209
1332
  minHeight: "40px",
1210
1333
  padding: "8px 16px",
1211
1334
  fontSize: "14px",
@@ -1217,6 +1340,9 @@ var styles = {
1217
1340
  cursor: "pointer"
1218
1341
  },
1219
1342
  settingsButton: {
1343
+ display: "inline-flex",
1344
+ alignItems: "center",
1345
+ gap: "8px",
1220
1346
  minHeight: "40px",
1221
1347
  padding: "8px 16px",
1222
1348
  fontSize: "14px",
@@ -1433,6 +1559,8 @@ function MoreIcon({ size = 18, color = "currentColor" }) {
1433
1559
  }
1434
1560
  );
1435
1561
  }
1562
+ var VIEWPORT_MARGIN = 8;
1563
+ var TRIGGER_GAP = 4;
1436
1564
  function MessageActionsMenu({
1437
1565
  message,
1438
1566
  onReply,
@@ -1444,16 +1572,43 @@ function MessageActionsMenu({
1444
1572
  className
1445
1573
  }) {
1446
1574
  const [isOpen, setIsOpen] = useState(false);
1447
- const wrapperRef = useRef(null);
1575
+ const [coords, setCoords] = useState(null);
1448
1576
  const triggerRef = useRef(null);
1577
+ const menuRef = useRef(null);
1449
1578
  const isPinned = !!message.isPinned;
1450
1579
  const canCopy = message.type === "text" && !!message.body;
1580
+ useLayoutEffect(() => {
1581
+ if (!isOpen) {
1582
+ setCoords(null);
1583
+ return;
1584
+ }
1585
+ const trigger = triggerRef.current;
1586
+ const menu2 = menuRef.current;
1587
+ if (!trigger || !menu2) return;
1588
+ const triggerRect = trigger.getBoundingClientRect();
1589
+ const menuRect = menu2.getBoundingClientRect();
1590
+ const vw = window.innerWidth;
1591
+ const vh = window.innerHeight;
1592
+ const spaceBelow = vh - triggerRect.bottom;
1593
+ const spaceAbove = triggerRect.top;
1594
+ const placeAbove = spaceBelow < menuRect.height + TRIGGER_GAP + VIEWPORT_MARGIN && spaceAbove > spaceBelow;
1595
+ const top = placeAbove ? triggerRect.top - menuRect.height - TRIGGER_GAP : triggerRect.bottom + TRIGGER_GAP;
1596
+ const preferredLeft = anchorRight ? triggerRect.right - menuRect.width : triggerRect.left;
1597
+ const left = Math.max(
1598
+ VIEWPORT_MARGIN,
1599
+ Math.min(preferredLeft, vw - menuRect.width - VIEWPORT_MARGIN)
1600
+ );
1601
+ const clampedTop = Math.max(
1602
+ VIEWPORT_MARGIN,
1603
+ Math.min(top, vh - menuRect.height - VIEWPORT_MARGIN)
1604
+ );
1605
+ setCoords({ top: clampedTop, left });
1606
+ }, [isOpen, anchorRight]);
1451
1607
  useEffect(() => {
1452
1608
  if (!isOpen) return;
1609
+ const isInside = (node) => !!node && (triggerRef.current?.contains(node) === true || menuRef.current?.contains(node) === true);
1453
1610
  const handleClick = (e) => {
1454
- if (!wrapperRef.current?.contains(e.target)) {
1455
- setIsOpen(false);
1456
- }
1611
+ if (!isInside(e.target)) setIsOpen(false);
1457
1612
  };
1458
1613
  const handleKey = (e) => {
1459
1614
  if (e.key === "Escape") {
@@ -1461,18 +1616,100 @@ function MessageActionsMenu({
1461
1616
  triggerRef.current?.focus();
1462
1617
  }
1463
1618
  };
1619
+ const handleScroll = (e) => {
1620
+ if (!isInside(e.target)) setIsOpen(false);
1621
+ };
1622
+ const handleResize = () => setIsOpen(false);
1464
1623
  document.addEventListener("mousedown", handleClick);
1465
1624
  document.addEventListener("keydown", handleKey);
1625
+ window.addEventListener("resize", handleResize);
1626
+ window.addEventListener("scroll", handleScroll, true);
1466
1627
  return () => {
1467
1628
  document.removeEventListener("mousedown", handleClick);
1468
1629
  document.removeEventListener("keydown", handleKey);
1630
+ window.removeEventListener("resize", handleResize);
1631
+ window.removeEventListener("scroll", handleScroll, true);
1469
1632
  };
1470
1633
  }, [isOpen]);
1471
1634
  const run = (fn) => {
1472
1635
  setIsOpen(false);
1473
1636
  fn();
1474
1637
  };
1475
- return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className, style: styles4.wrapper, children: [
1638
+ const menu = isOpen ? /* @__PURE__ */ jsxs(
1639
+ "div",
1640
+ {
1641
+ ref: menuRef,
1642
+ role: "menu",
1643
+ "aria-label": "Message actions",
1644
+ style: {
1645
+ ...styles4.menu,
1646
+ // Render off-screen on first paint so we can measure before placing,
1647
+ // then snap to computed coords on the next layout pass.
1648
+ top: coords?.top ?? -9999,
1649
+ left: coords?.left ?? -9999,
1650
+ visibility: coords ? "visible" : "hidden"
1651
+ },
1652
+ children: [
1653
+ /* @__PURE__ */ jsxs(
1654
+ "button",
1655
+ {
1656
+ type: "button",
1657
+ role: "menuitem",
1658
+ onClick: () => run(() => onReply(message)),
1659
+ style: styles4.item,
1660
+ children: [
1661
+ /* @__PURE__ */ jsx(ReplyIcon, { size: 18, color: "#374151" }),
1662
+ /* @__PURE__ */ jsx("span", { children: "Reply" })
1663
+ ]
1664
+ }
1665
+ ),
1666
+ isPinned ? /* @__PURE__ */ jsxs(
1667
+ "button",
1668
+ {
1669
+ type: "button",
1670
+ role: "menuitem",
1671
+ onClick: () => run(() => onUnpin(message.id)),
1672
+ style: styles4.item,
1673
+ children: [
1674
+ /* @__PURE__ */ jsx(PinIcon, { size: 16, color: "#374151" }),
1675
+ /* @__PURE__ */ jsx("span", { children: "Unpin" })
1676
+ ]
1677
+ }
1678
+ ) : /* @__PURE__ */ jsxs(
1679
+ "button",
1680
+ {
1681
+ type: "button",
1682
+ role: "menuitem",
1683
+ onClick: () => run(() => onPin(message.id)),
1684
+ disabled: pinDisabled,
1685
+ style: {
1686
+ ...styles4.item,
1687
+ ...pinDisabled ? styles4.itemDisabled : {}
1688
+ },
1689
+ title: pinDisabled ? "Pin limit reached (max 3)" : void 0,
1690
+ children: [
1691
+ /* @__PURE__ */ jsx(PinIcon, { size: 16, color: "#374151" }),
1692
+ /* @__PURE__ */ jsx("span", { children: pinDisabled ? "Pin (limit reached)" : "Pin" })
1693
+ ]
1694
+ }
1695
+ ),
1696
+ canCopy && onCopy && /* @__PURE__ */ jsxs(
1697
+ "button",
1698
+ {
1699
+ type: "button",
1700
+ role: "menuitem",
1701
+ onClick: () => run(() => onCopy(message.body)),
1702
+ style: styles4.item,
1703
+ children: [
1704
+ /* @__PURE__ */ jsx(CopyIcon, { size: 16, color: "#374151" }),
1705
+ /* @__PURE__ */ jsx("span", { children: "Copy text" })
1706
+ ]
1707
+ }
1708
+ )
1709
+ ]
1710
+ }
1711
+ ) : null;
1712
+ return /* @__PURE__ */ jsxs("div", { className, style: styles4.wrapper, children: [
1476
1713
  /* @__PURE__ */ jsx(
1477
1714
  "button",
1478
1715
  {
@@ -1490,75 +1727,7 @@ function MessageActionsMenu({
1490
1727
  children: /* @__PURE__ */ jsx(MoreIcon, { size: 18, color: "#4b5563" })
1491
1728
  }
1492
1729
  ),
1493
- isOpen && /* @__PURE__ */ jsxs(
1494
- "div",
1495
- {
1496
- role: "menu",
1497
- "aria-label": "Message actions",
1498
- style: {
1499
- ...styles4.menu,
1500
- ...anchorRight ? styles4.menuRight : styles4.menuLeft
1501
- },
1502
- children: [
1503
- /* @__PURE__ */ jsxs(
1504
- "button",
1505
- {
1506
- type: "button",
1507
- role: "menuitem",
1508
- onClick: () => run(() => onReply(message)),
1509
- style: styles4.item,
1510
- children: [
1511
- /* @__PURE__ */ jsx(ReplyIcon, { size: 18, color: "#374151" }),
1512
- /* @__PURE__ */ jsx("span", { children: "Reply" })
1513
- ]
1514
- }
1515
- ),
1516
- isPinned ? /* @__PURE__ */ jsxs(
1517
- "button",
1518
- {
1519
- type: "button",
1520
- role: "menuitem",
1521
- onClick: () => run(() => onUnpin(message.id)),
1522
- style: styles4.item,
1523
- children: [
1524
- /* @__PURE__ */ jsx(PinIcon, { size: 16, color: "#374151" }),
1525
- /* @__PURE__ */ jsx("span", { children: "Unpin" })
1526
- ]
1527
- }
1528
- ) : /* @__PURE__ */ jsxs(
1529
- "button",
1530
- {
1531
- type: "button",
1532
- role: "menuitem",
1533
- onClick: () => run(() => onPin(message.id)),
1534
- disabled: pinDisabled,
1535
- style: {
1536
- ...styles4.item,
1537
- ...pinDisabled ? styles4.itemDisabled : {}
1538
- },
1539
- title: pinDisabled ? "Pin limit reached (max 3)" : void 0,
1540
- children: [
1541
- /* @__PURE__ */ jsx(PinIcon, { size: 16, color: "#374151" }),
1542
- /* @__PURE__ */ jsx("span", { children: pinDisabled ? "Pin (limit reached)" : "Pin" })
1543
- ]
1544
- }
1545
- ),
1546
- canCopy && onCopy && /* @__PURE__ */ jsxs(
1547
- "button",
1548
- {
1549
- type: "button",
1550
- role: "menuitem",
1551
- onClick: () => run(() => onCopy(message.body)),
1552
- style: styles4.item,
1553
- children: [
1554
- /* @__PURE__ */ jsx(CopyIcon, { size: 16, color: "#374151" }),
1555
- /* @__PURE__ */ jsx("span", { children: "Copy text" })
1556
- ]
1557
- }
1558
- )
1559
- ]
1560
- }
1561
- )
1730
+ menu && typeof document !== "undefined" ? createPortal(menu, document.body) : null
1562
1731
  ] });
1563
1732
  }
1564
1733
  var styles4 = {
@@ -1579,22 +1748,15 @@ var styles4 = {
1579
1748
  color: "#4b5563"
1580
1749
  },
1581
1750
  menu: {
1582
- position: "absolute",
1583
- top: "100%",
1584
- marginTop: "4px",
1751
+ position: "fixed",
1585
1752
  minWidth: "160px",
1586
1753
  backgroundColor: "#ffffff",
1587
1754
  border: "1px solid #e5e7eb",
1588
1755
  borderRadius: "10px",
1589
1756
  boxShadow: "0 6px 18px rgba(0, 0, 0, 0.12)",
1590
1757
  padding: "4px",
1591
- zIndex: 20
1592
- },
1593
- menuLeft: {
1594
- left: 0
1595
- },
1596
- menuRight: {
1597
- right: 0
1758
+ // Above the floating popup (z-index 9999) but below any future modal.
1759
+ zIndex: 1e4
1598
1760
  },
1599
1761
  item: {
1600
1762
  display: "flex",
@@ -2247,31 +2409,34 @@ var MessageList = forwardRef(function MessageList2({
2247
2409
  /* @__PURE__ */ jsx("p", { style: styles6.emptySubtitle, children: "Start the conversation \u2014 everyone on this case will see what you write." })
2248
2410
  ] }),
2249
2411
  messages.length === 0 && isLoading && /* @__PURE__ */ jsx("div", { style: styles6.firstLoad, children: /* @__PURE__ */ jsx(Spinner, { size: 20 }) }),
2250
- messages.map((message, index) => {
2251
- const currentBucket = dayBucketKey(message.insertedAt);
2252
- const prevBucket = index === 0 ? null : dayBucketKey(messages[index - 1].insertedAt);
2253
- const showDivider = currentBucket && currentBucket !== prevBucket;
2254
- return /* @__PURE__ */ jsxs(React4.Fragment, { children: [
2255
- showDivider && /* @__PURE__ */ jsx("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: /* @__PURE__ */ jsx("span", { style: styles6.dayDividerPill, children: formatDayDivider(message.insertedAt) }) }),
2256
- /* @__PURE__ */ jsx("div", { "data-message-id": message.id, children: /* @__PURE__ */ jsx(
2257
- MessageBubble,
2258
- {
2259
- message,
2260
- isOwn: message.senderId === currentUserId,
2261
- participants,
2262
- currentUserId,
2263
- showSeenBy,
2264
- onDeepLinkClick,
2265
- onReply,
2266
- onPin,
2267
- onUnpin,
2268
- onCopy,
2269
- onRetry,
2270
- pinDisabled
2271
- }
2272
- ) })
2273
- ] }, message.id);
2274
- }),
2412
+ (() => {
2413
+ let lastValidBucket = null;
2414
+ return messages.map((message) => {
2415
+ const currentBucket = dayBucketKey(message.insertedAt);
2416
+ const showDivider = !!currentBucket && currentBucket !== lastValidBucket;
2417
+ if (currentBucket) lastValidBucket = currentBucket;
2418
+ return /* @__PURE__ */ jsxs(React4.Fragment, { children: [
2419
+ showDivider && /* @__PURE__ */ jsx("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: /* @__PURE__ */ jsx("span", { style: styles6.dayDividerPill, children: formatDayDivider(message.insertedAt) }) }),
2420
+ /* @__PURE__ */ jsx("div", { "data-message-id": message.id, "data-natoe-message-bubble": true, style: styles6.bubbleSlot, children: /* @__PURE__ */ jsx(
2421
+ MessageBubble,
2422
+ {
2423
+ message,
2424
+ isOwn: message.senderId === currentUserId,
2425
+ participants,
2426
+ currentUserId,
2427
+ showSeenBy,
2428
+ onDeepLinkClick,
2429
+ onReply,
2430
+ onPin,
2431
+ onUnpin,
2432
+ onCopy,
2433
+ onRetry,
2434
+ pinDisabled
2435
+ }
2436
+ ) })
2437
+ ] }, message.id);
2438
+ });
2439
+ })(),
2275
2440
  typingUsers.length > 0 && /* @__PURE__ */ jsxs("div", { style: styles6.typingIndicator, children: [
2276
2441
  typingUsers.map((t) => t.userName ?? "Someone").join(", "),
2277
2442
  typingUsers.length === 1 ? " is " : " are ",
@@ -2351,7 +2516,11 @@ var styles6 = {
2351
2516
  dayDivider: {
2352
2517
  display: "flex",
2353
2518
  justifyContent: "center",
2354
- margin: "12px 0 8px"
2519
+ margin: "12px 0 8px",
2520
+ position: "sticky",
2521
+ top: "4px",
2522
+ zIndex: 1,
2523
+ pointerEvents: "none"
2355
2524
  },
2356
2525
  dayDividerPill: {
2357
2526
  display: "inline-block",
@@ -2361,7 +2530,11 @@ var styles6 = {
2361
2530
  color: "#334155",
2362
2531
  backgroundColor: "#e2e8f0",
2363
2532
  borderRadius: "12px",
2364
- letterSpacing: "0.2px"
2533
+ letterSpacing: "0.2px",
2534
+ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.04)"
2535
+ },
2536
+ bubbleSlot: {
2537
+ animation: "natoe-colab-message-in 180ms ease-out"
2365
2538
  }
2366
2539
  };
2367
2540
  function ReplyPreview({ message, onCancel, className }) {
@@ -3413,11 +3586,52 @@ var styles12 = {
3413
3586
  letterSpacing: "0.4px"
3414
3587
  }
3415
3588
  };
3589
+
3590
+ // src/core/styles.ts
3591
+ var FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
3592
+ var ROOT_CLASS = "natoe-colab-root";
3593
+ var GLOBAL_STYLE_ID = "natoe-colab-global-styles";
3594
+ var GLOBAL_CSS = `
3595
+ @keyframes natoe-colab-spin { to { transform: rotate(360deg); } }
3596
+
3597
+ @keyframes natoe-colab-message-in {
3598
+ from { opacity: 0; transform: translateY(4px); }
3599
+ to { opacity: 1; transform: translateY(0); }
3600
+ }
3601
+
3602
+ /* High-contrast adjustments \u2014 older users on OS-level high-contrast mode
3603
+ get heavier borders and stronger text without losing the package look. */
3604
+ @media (prefers-contrast: more) {
3605
+ .${ROOT_CLASS} {
3606
+ color: #000000;
3607
+ }
3608
+ .${ROOT_CLASS} button {
3609
+ outline: 1px solid currentColor;
3610
+ }
3611
+ }
3612
+
3613
+ /* Honour reduced-motion preferences by killing entry animations. */
3614
+ @media (prefers-reduced-motion: reduce) {
3615
+ .${ROOT_CLASS} [data-natoe-message-bubble] {
3616
+ animation: none !important;
3617
+ }
3618
+ }
3619
+ `;
3620
+ function ensureGlobalStyles() {
3621
+ if (typeof document === "undefined") return;
3622
+ if (document.getElementById(GLOBAL_STYLE_ID)) return;
3623
+ const style = document.createElement("style");
3624
+ style.id = GLOBAL_STYLE_ID;
3625
+ style.textContent = GLOBAL_CSS;
3626
+ document.head.appendChild(style);
3627
+ }
3628
+ ensureGlobalStyles();
3416
3629
  function CollabPanel({
3417
3630
  orderId,
3418
3631
  patientData,
3419
3632
  participantIds,
3420
3633
  showSeenBy = true,
3634
+ onBack,
3421
3635
  className,
3422
3636
  style
3423
3637
  }) {
@@ -3491,13 +3705,13 @@ function CollabPanel({
3491
3705
  };
3492
3706
  const pinDisabled = pinnedMessages.length >= MAX_PINNED_MESSAGES;
3493
3707
  if (error) {
3494
- return /* @__PURE__ */ jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxs("div", { style: panelStyles.errorState, children: [
3708
+ return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxs("div", { style: panelStyles.errorState, children: [
3495
3709
  /* @__PURE__ */ jsx("p", { style: panelStyles.errorTitle, children: "Unable to load conversation" }),
3496
3710
  /* @__PURE__ */ jsx("p", { style: panelStyles.errorMessage, children: error })
3497
3711
  ] }) });
3498
3712
  }
3499
3713
  if (isLoading && !conversation) {
3500
- return /* @__PURE__ */ jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
3714
+ 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..." }) });
3501
3715
  }
3502
3716
  return /* @__PURE__ */ jsxs("div", { className, style: { ...panelStyles.container, ...style }, children: [
3503
3717
  showSettings && conversation ? /* @__PURE__ */ jsx(
@@ -3525,7 +3739,8 @@ function CollabPanel({
3525
3739
  patientData,
3526
3740
  participants,
3527
3741
  onOpenDicom: handleOpenDicom,
3528
- onOpenSettings: () => setShowSettings(true)
3742
+ onOpenSettings: () => setShowSettings(true),
3743
+ onBack
3529
3744
  }
3530
3745
  ),
3531
3746
  pinnedMessages.length > 0 && /* @__PURE__ */ jsx(
@@ -3583,7 +3798,7 @@ var panelStyles = {
3583
3798
  borderRadius: "8px",
3584
3799
  overflow: "hidden",
3585
3800
  border: "1px solid #e5e7eb",
3586
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
3801
+ fontFamily: FONT_STACK
3587
3802
  },
3588
3803
  loadingState: {
3589
3804
  display: "flex",
@@ -3642,6 +3857,7 @@ function ExpandIcon({ size = 18, color = "currentColor" }) {
3642
3857
  }
3643
3858
  );
3644
3859
  }
3860
+ ensureGlobalStyles();
3645
3861
  var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3646
3862
  function CollabPopup({
3647
3863
  orderId,
@@ -3649,6 +3865,7 @@ function CollabPopup({
3649
3865
  participantIds,
3650
3866
  isOpen,
3651
3867
  onClose,
3868
+ onBack,
3652
3869
  initialPosition,
3653
3870
  width = 380,
3654
3871
  height = 520,
@@ -3742,7 +3959,7 @@ function CollabPopup({
3742
3959
  "div",
3743
3960
  {
3744
3961
  ref: containerRef,
3745
- className,
3962
+ className: [ROOT_CLASS, className].filter(Boolean).join(" "),
3746
3963
  role: "dialog",
3747
3964
  "aria-modal": "true",
3748
3965
  "aria-labelledby": titleId,
@@ -3758,7 +3975,18 @@ function CollabPopup({
3758
3975
  onMouseDown: handleMouseDown,
3759
3976
  children: [
3760
3977
  /* @__PURE__ */ jsxs("div", { "data-drag-handle": true, style: styles13.titleBar, children: [
3761
- /* @__PURE__ */ jsx("span", { id: titleId, style: styles13.titleText, children: buildChannelName(patientData) }),
3978
+ onBack && /* @__PURE__ */ jsx(
3979
+ "button",
3980
+ {
3981
+ onClick: onBack,
3982
+ style: styles13.titleButton,
3983
+ "aria-label": "Go back",
3984
+ title: "Back",
3985
+ type: "button",
3986
+ children: "\u2190"
3987
+ }
3988
+ ),
3989
+ /* @__PURE__ */ jsx("span", { id: titleId, style: { ...styles13.titleText, flex: 1 }, children: buildChannelName(patientData) }),
3762
3990
  /* @__PURE__ */ jsxs("div", { style: styles13.titleActions, children: [
3763
3991
  /* @__PURE__ */ jsx(
3764
3992
  "button",
@@ -3800,7 +4028,8 @@ var styles13 = {
3800
4028
  flexDirection: "column",
3801
4029
  backgroundColor: "#ffffff",
3802
4030
  border: "1px solid #e5e7eb",
3803
- transition: "height 0.2s ease"
4031
+ transition: "height 0.2s ease",
4032
+ fontFamily: FONT_STACK
3804
4033
  },
3805
4034
  titleBar: {
3806
4035
  display: "flex",
@@ -4045,8 +4274,8 @@ function useInlineCollab({
4045
4274
  await dispatchSend({
4046
4275
  body: "Voice message",
4047
4276
  type: "audio",
4048
- mediaUrl: url,
4049
- mediaDuration: duration
4277
+ media_url: url,
4278
+ media_duration: duration
4050
4279
  });
4051
4280
  } catch (err) {
4052
4281
  setError(err instanceof Error ? err.message : "Failed to send voice message");
@@ -4072,6 +4301,7 @@ function useInlineCollab({
4072
4301
  containerRef
4073
4302
  };
4074
4303
  }
4304
+ ensureGlobalStyles();
4075
4305
  function CollabInline({
4076
4306
  orderId,
4077
4307
  patientData,
@@ -4094,9 +4324,9 @@ function CollabInline({
4094
4324
  containerRef
4095
4325
  } = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
4096
4326
  if (isLoading && !hasConversation) {
4097
- return /* @__PURE__ */ jsx("div", { className, style: { ...styles14.container, ...style }, children: /* @__PURE__ */ jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4327
+ return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: /* @__PURE__ */ jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4098
4328
  }
4099
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className, style: { ...styles14.container, ...style }, children: [
4329
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: [
4100
4330
  hasConversation && messages.length > 0 && /* @__PURE__ */ jsx("div", { style: styles14.preview, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsx(InlineMessageRow, { message }, message.id)) }),
4101
4331
  error && /* @__PURE__ */ jsx("div", { style: styles14.error, children: error }),
4102
4332
  /* @__PURE__ */ jsx(
@@ -4312,7 +4542,7 @@ var styles14 = {
4312
4542
  backgroundColor: "#ffffff",
4313
4543
  border: "1px solid #e5e7eb",
4314
4544
  borderRadius: "8px",
4315
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
4545
+ fontFamily: FONT_STACK
4316
4546
  },
4317
4547
  loadingState: {
4318
4548
  padding: "8px",
@@ -4942,6 +5172,8 @@ var styles16 = {
4942
5172
  textAlign: "center"
4943
5173
  }
4944
5174
  };
5175
+ ensureGlobalStyles();
5176
+ var COMPACT_BREAKPOINT = 720;
4945
5177
  function CollabInbox({
4946
5178
  initialConversationId,
4947
5179
  onSelectConversation,
@@ -4951,34 +5183,55 @@ function CollabInbox({
4951
5183
  }) {
4952
5184
  const { conversations, isLoading, error } = useConversationList();
4953
5185
  const [selectedId, setSelectedId] = useState(initialConversationId ?? null);
5186
+ const [isCompact, setIsCompact] = useState(
5187
+ () => typeof window === "undefined" ? false : window.innerWidth < COMPACT_BREAKPOINT
5188
+ );
5189
+ useEffect(() => {
5190
+ if (typeof window === "undefined") return;
5191
+ const mq = window.matchMedia(`(max-width: ${COMPACT_BREAKPOINT - 1}px)`);
5192
+ const sync = () => setIsCompact(mq.matches);
5193
+ sync();
5194
+ mq.addEventListener("change", sync);
5195
+ return () => mq.removeEventListener("change", sync);
5196
+ }, []);
4954
5197
  const selected = conversations.find((c) => c.id === selectedId) || null;
4955
5198
  const handleSelect = (item) => {
4956
5199
  setSelectedId(item.id);
4957
5200
  onSelectConversation?.(item);
4958
5201
  };
4959
- return /* @__PURE__ */ jsxs("div", { className, style: { ...styles17.container, ...style }, children: [
4960
- /* @__PURE__ */ jsx("div", { style: styles17.sidebar, children: /* @__PURE__ */ jsx(
4961
- ConversationList,
4962
- {
4963
- conversations,
4964
- selectedId,
4965
- isLoading,
4966
- error,
4967
- onSelect: handleSelect,
4968
- title
4969
- }
4970
- ) }),
4971
- /* @__PURE__ */ jsx("div", { style: styles17.main, children: selected ? /* @__PURE__ */ jsx(
4972
- CollabPanel,
4973
- {
4974
- orderId: selected.orderId,
4975
- patientData: buildPatientData(selected),
4976
- showSeenBy: true,
4977
- style: styles17.panel
4978
- },
4979
- selected.id
4980
- ) : /* @__PURE__ */ jsx(EmptyState, { hasAny: conversations.length > 0 }) })
4981
- ] });
5202
+ const showDetail = !isCompact || isCompact && !!selected;
5203
+ const showList = !isCompact || isCompact && !selected;
5204
+ return /* @__PURE__ */ jsxs(
5205
+ "div",
5206
+ {
5207
+ className: [ROOT_CLASS, className].filter(Boolean).join(" "),
5208
+ style: { ...styles17.container, ...style },
5209
+ children: [
5210
+ showList && /* @__PURE__ */ jsx("div", { style: isCompact ? styles17.sidebarCompact : styles17.sidebar, children: /* @__PURE__ */ jsx(
5211
+ ConversationList,
5212
+ {
5213
+ conversations,
5214
+ selectedId,
5215
+ isLoading,
5216
+ error,
5217
+ onSelect: handleSelect,
5218
+ title
5219
+ }
5220
+ ) }),
5221
+ showDetail && /* @__PURE__ */ jsx("div", { style: isCompact ? styles17.mainCompact : styles17.main, children: selected ? /* @__PURE__ */ jsx(
5222
+ CollabPanel,
5223
+ {
5224
+ orderId: selected.orderId,
5225
+ patientData: buildPatientData(selected),
5226
+ showSeenBy: true,
5227
+ style: styles17.panel,
5228
+ onBack: isCompact ? () => setSelectedId(null) : void 0
5229
+ },
5230
+ selected.id
5231
+ ) : /* @__PURE__ */ jsx(EmptyState, { hasAny: conversations.length > 0 }) })
5232
+ ]
5233
+ }
5234
+ );
4982
5235
  }
4983
5236
  function buildPatientData(item) {
4984
5237
  if (item.patientSnapshot) return item.patientSnapshot;
@@ -5000,7 +5253,7 @@ var styles17 = {
5000
5253
  height: "100%",
5001
5254
  width: "100%",
5002
5255
  backgroundColor: "#ffffff",
5003
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
5256
+ fontFamily: FONT_STACK
5004
5257
  },
5005
5258
  sidebar: {
5006
5259
  width: "320px",
@@ -5009,6 +5262,12 @@ var styles17 = {
5009
5262
  display: "flex",
5010
5263
  flexDirection: "column"
5011
5264
  },
5265
+ sidebarCompact: {
5266
+ width: "100%",
5267
+ flex: 1,
5268
+ display: "flex",
5269
+ flexDirection: "column"
5270
+ },
5012
5271
  main: {
5013
5272
  flex: 1,
5014
5273
  display: "flex",
@@ -5017,6 +5276,13 @@ var styles17 = {
5017
5276
  padding: "12px",
5018
5277
  backgroundColor: "#f3f4f6"
5019
5278
  },
5279
+ mainCompact: {
5280
+ flex: 1,
5281
+ display: "flex",
5282
+ flexDirection: "column",
5283
+ minWidth: 0,
5284
+ backgroundColor: "#f3f4f6"
5285
+ },
5020
5286
  panel: {
5021
5287
  height: "100%",
5022
5288
  width: "100%"