@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.js CHANGED
@@ -3,6 +3,7 @@
3
3
  var React4 = require('react');
4
4
  var phoenix = require('phoenix');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
+ var reactDom = require('react-dom');
6
7
 
7
8
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
9
 
@@ -376,7 +377,9 @@ function CollabProvider({ config, apiBaseUrl, children }) {
376
377
  const data = snakeToCamel(await response.json());
377
378
  orderIds.forEach((orderId) => {
378
379
  const preview = data.previews[orderId] ?? null;
379
- previewCache.current.set(orderId, preview);
380
+ if (preview !== null) {
381
+ previewCache.current.set(orderId, preview);
382
+ }
380
383
  resolvers.get(orderId)?.forEach((resolve) => resolve(preview));
381
384
  });
382
385
  } catch (error) {
@@ -423,7 +426,8 @@ function CollabProvider({ config, apiBaseUrl, children }) {
423
426
  if (!response.ok) {
424
427
  throw { code: "FETCH_ERROR", message: "Failed to fetch messages", status: response.status };
425
428
  }
426
- return snakeToCamel(await response.json());
429
+ const msgs = snakeToCamel(await response.json());
430
+ return msgs.sort((a, b) => a.insertedAt.localeCompare(b.insertedAt));
427
431
  },
428
432
  [apiBaseUrl, authHeaders]
429
433
  );
@@ -897,8 +901,8 @@ function useConversation({
897
901
  const payload = {
898
902
  body: "Voice message",
899
903
  type: "audio",
900
- mediaUrl: url,
901
- mediaDuration: duration
904
+ media_url: url,
905
+ media_duration: duration
902
906
  };
903
907
  const { conv, persistedByCreate } = await ensureConversation(payload);
904
908
  if (persistedByCreate) return;
@@ -914,8 +918,8 @@ function useConversation({
914
918
  const payload = {
915
919
  body: file.name,
916
920
  type: isImage ? "image" : "file",
917
- mediaUrl: url,
918
- fileName: file.name
921
+ media_url: url,
922
+ file_name: file.name
919
923
  };
920
924
  const { conv, persistedByCreate } = await ensureConversation(payload);
921
925
  if (persistedByCreate) return;
@@ -1147,71 +1151,190 @@ function useDeepLinks() {
1147
1151
  function escapeRegex(string) {
1148
1152
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1149
1153
  }
1154
+ function DicomIcon({ size = 18, color = "currentColor" }) {
1155
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1156
+ "svg",
1157
+ {
1158
+ xmlns: "http://www.w3.org/2000/svg",
1159
+ width: size,
1160
+ height: size,
1161
+ viewBox: "0 0 24 24",
1162
+ fill: "none",
1163
+ stroke: color,
1164
+ strokeWidth: "2",
1165
+ strokeLinecap: "round",
1166
+ strokeLinejoin: "round",
1167
+ "aria-hidden": "true",
1168
+ children: [
1169
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "4", y: "4", width: "13", height: "13", rx: "2" }),
1170
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 7h13a2 2 0 0 1 2 2v11", opacity: "0.6" }),
1171
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "9.5", r: "1.2", fill: color, stroke: "none" }),
1172
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 14l3-3 4 4 2-2 3 3" })
1173
+ ]
1174
+ }
1175
+ );
1176
+ }
1177
+ function SettingsIcon({ size = 18, color = "currentColor" }) {
1178
+ return /* @__PURE__ */ jsxRuntime.jsx(
1179
+ "svg",
1180
+ {
1181
+ xmlns: "http://www.w3.org/2000/svg",
1182
+ width: size,
1183
+ height: size,
1184
+ viewBox: "0 0 24 24",
1185
+ fill: color,
1186
+ "aria-hidden": "true",
1187
+ children: /* @__PURE__ */ jsxRuntime.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" })
1188
+ }
1189
+ );
1190
+ }
1191
+ function BackIcon({ size = 22, color = "currentColor" }) {
1192
+ return /* @__PURE__ */ jsxRuntime.jsx(
1193
+ "svg",
1194
+ {
1195
+ xmlns: "http://www.w3.org/2000/svg",
1196
+ width: size,
1197
+ height: size,
1198
+ viewBox: "0 0 24 24",
1199
+ fill: color,
1200
+ "aria-hidden": "true",
1201
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" })
1202
+ }
1203
+ );
1204
+ }
1150
1205
  function PatientHeader({
1151
1206
  patientData,
1152
1207
  onOpenDicom,
1153
1208
  onOpenSettings,
1209
+ onBack,
1154
1210
  className
1155
1211
  }) {
1156
1212
  const hasDicom = !!(patientData.studyId && patientData.storageId);
1213
+ const meta = [];
1214
+ if (patientData.patientAge) meta.push({ label: "Age", value: String(patientData.patientAge) });
1215
+ if (patientData.patientSex) meta.push({ label: "Sex", value: String(patientData.patientSex) });
1216
+ if (patientData.studyType) meta.push({ label: "Modality", value: patientData.studyType });
1217
+ if (patientData.bodyParts && patientData.bodyParts.length > 0) {
1218
+ meta.push({ label: "Body part", value: patientData.bodyParts.join(", ") });
1219
+ }
1220
+ if (patientData.referringPhysician) {
1221
+ meta.push({ label: "Ref", value: patientData.referringPhysician });
1222
+ }
1157
1223
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles.container, children: [
1158
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.infoRow, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.patientInfo, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.patientName, children: buildChannelName(patientData) }) }) }),
1159
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.secondaryRow, children: [
1160
- patientData.patientAge && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.secondary, children: [
1161
- patientData.patientAge,
1162
- patientData.patientSex ? `/${patientData.patientSex}` : ""
1163
- ] }),
1164
- patientData.studyType && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.secondary, children: patientData.studyType }),
1165
- patientData.bodyParts && patientData.bodyParts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.secondary, children: patientData.bodyParts.join(", ") }),
1166
- patientData.referringPhysician && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.secondary, children: [
1167
- "Ref: ",
1168
- patientData.referringPhysician
1169
- ] })
1224
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.nameRow, children: [
1225
+ onBack && /* @__PURE__ */ jsxRuntime.jsx(
1226
+ "button",
1227
+ {
1228
+ type: "button",
1229
+ onClick: onBack,
1230
+ style: styles.backButton,
1231
+ "aria-label": "Back to conversations",
1232
+ title: "Back to conversations",
1233
+ children: /* @__PURE__ */ jsxRuntime.jsx(BackIcon, { size: 22, color: "#1f2937" })
1234
+ }
1235
+ ),
1236
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.nameText, children: buildChannelName(patientData) })
1170
1237
  ] }),
1171
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actions, children: [
1172
- hasDicom && onOpenDicom && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onOpenDicom, style: styles.dicomButton, type: "button", children: "View DICOM" }),
1173
- onOpenSettings && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onOpenSettings, style: styles.settingsButton, type: "button", children: "Settings" })
1174
- ] })
1238
+ meta.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.metaRow, children: meta.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.metaItem, children: [
1239
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.metaLabel, children: [
1240
+ item.label,
1241
+ ":"
1242
+ ] }),
1243
+ " ",
1244
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.metaValue, children: item.value })
1245
+ ] }, item.label)) }),
1246
+ hasDicom && onOpenDicom || onOpenSettings ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actions, children: [
1247
+ hasDicom && onOpenDicom && /* @__PURE__ */ jsxRuntime.jsxs(
1248
+ "button",
1249
+ {
1250
+ onClick: onOpenDicom,
1251
+ style: styles.dicomButton,
1252
+ type: "button",
1253
+ "aria-label": "View DICOM study",
1254
+ children: [
1255
+ /* @__PURE__ */ jsxRuntime.jsx(DicomIcon, { size: 18, color: "#ffffff" }),
1256
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "View DICOM" })
1257
+ ]
1258
+ }
1259
+ ),
1260
+ onOpenSettings && /* @__PURE__ */ jsxRuntime.jsxs(
1261
+ "button",
1262
+ {
1263
+ onClick: onOpenSettings,
1264
+ style: styles.settingsButton,
1265
+ type: "button",
1266
+ "aria-label": "Open channel settings",
1267
+ children: [
1268
+ /* @__PURE__ */ jsxRuntime.jsx(SettingsIcon, { size: 18, color: "#374151" }),
1269
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Settings" })
1270
+ ]
1271
+ }
1272
+ )
1273
+ ] }) : null
1175
1274
  ] });
1176
1275
  }
1177
1276
  var styles = {
1178
1277
  container: {
1179
- padding: "12px 16px",
1278
+ padding: "14px 16px",
1180
1279
  borderBottom: "1px solid #e5e7eb",
1181
1280
  backgroundColor: "#f9fafb"
1182
1281
  },
1183
- infoRow: {
1282
+ nameRow: {
1184
1283
  display: "flex",
1185
- justifyContent: "space-between",
1186
- alignItems: "center"
1284
+ alignItems: "center",
1285
+ gap: "6px"
1187
1286
  },
1188
- patientInfo: {
1287
+ backButton: {
1288
+ width: "36px",
1289
+ height: "36px",
1189
1290
  display: "flex",
1190
1291
  alignItems: "center",
1191
- gap: "8px",
1192
- flexWrap: "wrap"
1292
+ justifyContent: "center",
1293
+ marginLeft: "-8px",
1294
+ backgroundColor: "transparent",
1295
+ border: "none",
1296
+ borderRadius: "50%",
1297
+ cursor: "pointer",
1298
+ flexShrink: 0
1193
1299
  },
1194
- patientName: {
1300
+ nameText: {
1301
+ fontSize: "18px",
1195
1302
  fontWeight: 600,
1196
- fontSize: "17px",
1197
- color: "#111827"
1303
+ color: "#111827",
1304
+ lineHeight: 1.3,
1305
+ minWidth: 0,
1306
+ overflow: "hidden",
1307
+ textOverflow: "ellipsis",
1308
+ whiteSpace: "nowrap"
1198
1309
  },
1199
- secondaryRow: {
1310
+ metaRow: {
1200
1311
  display: "flex",
1201
- gap: "12px",
1202
- marginTop: "4px",
1203
- alignItems: "center"
1312
+ flexWrap: "wrap",
1313
+ columnGap: "14px",
1314
+ rowGap: "4px",
1315
+ marginTop: "6px"
1204
1316
  },
1205
- secondary: {
1317
+ metaItem: {
1206
1318
  fontSize: "13px",
1207
- color: "#4b5563"
1319
+ color: "#374151",
1320
+ whiteSpace: "nowrap"
1321
+ },
1322
+ metaLabel: {
1323
+ color: "#6b7280",
1324
+ fontWeight: 500
1325
+ },
1326
+ metaValue: {
1327
+ color: "#111827"
1208
1328
  },
1209
1329
  actions: {
1210
1330
  display: "flex",
1211
1331
  gap: "8px",
1212
- marginTop: "8px"
1332
+ marginTop: "10px"
1213
1333
  },
1214
1334
  dicomButton: {
1335
+ display: "inline-flex",
1336
+ alignItems: "center",
1337
+ gap: "8px",
1215
1338
  minHeight: "40px",
1216
1339
  padding: "8px 16px",
1217
1340
  fontSize: "14px",
@@ -1223,6 +1346,9 @@ var styles = {
1223
1346
  cursor: "pointer"
1224
1347
  },
1225
1348
  settingsButton: {
1349
+ display: "inline-flex",
1350
+ alignItems: "center",
1351
+ gap: "8px",
1226
1352
  minHeight: "40px",
1227
1353
  padding: "8px 16px",
1228
1354
  fontSize: "14px",
@@ -1439,6 +1565,8 @@ function MoreIcon({ size = 18, color = "currentColor" }) {
1439
1565
  }
1440
1566
  );
1441
1567
  }
1568
+ var VIEWPORT_MARGIN = 8;
1569
+ var TRIGGER_GAP = 4;
1442
1570
  function MessageActionsMenu({
1443
1571
  message,
1444
1572
  onReply,
@@ -1450,16 +1578,43 @@ function MessageActionsMenu({
1450
1578
  className
1451
1579
  }) {
1452
1580
  const [isOpen, setIsOpen] = React4.useState(false);
1453
- const wrapperRef = React4.useRef(null);
1581
+ const [coords, setCoords] = React4.useState(null);
1454
1582
  const triggerRef = React4.useRef(null);
1583
+ const menuRef = React4.useRef(null);
1455
1584
  const isPinned = !!message.isPinned;
1456
1585
  const canCopy = message.type === "text" && !!message.body;
1586
+ React4.useLayoutEffect(() => {
1587
+ if (!isOpen) {
1588
+ setCoords(null);
1589
+ return;
1590
+ }
1591
+ const trigger = triggerRef.current;
1592
+ const menu2 = menuRef.current;
1593
+ if (!trigger || !menu2) return;
1594
+ const triggerRect = trigger.getBoundingClientRect();
1595
+ const menuRect = menu2.getBoundingClientRect();
1596
+ const vw = window.innerWidth;
1597
+ const vh = window.innerHeight;
1598
+ const spaceBelow = vh - triggerRect.bottom;
1599
+ const spaceAbove = triggerRect.top;
1600
+ const placeAbove = spaceBelow < menuRect.height + TRIGGER_GAP + VIEWPORT_MARGIN && spaceAbove > spaceBelow;
1601
+ const top = placeAbove ? triggerRect.top - menuRect.height - TRIGGER_GAP : triggerRect.bottom + TRIGGER_GAP;
1602
+ const preferredLeft = anchorRight ? triggerRect.right - menuRect.width : triggerRect.left;
1603
+ const left = Math.max(
1604
+ VIEWPORT_MARGIN,
1605
+ Math.min(preferredLeft, vw - menuRect.width - VIEWPORT_MARGIN)
1606
+ );
1607
+ const clampedTop = Math.max(
1608
+ VIEWPORT_MARGIN,
1609
+ Math.min(top, vh - menuRect.height - VIEWPORT_MARGIN)
1610
+ );
1611
+ setCoords({ top: clampedTop, left });
1612
+ }, [isOpen, anchorRight]);
1457
1613
  React4.useEffect(() => {
1458
1614
  if (!isOpen) return;
1615
+ const isInside = (node) => !!node && (triggerRef.current?.contains(node) === true || menuRef.current?.contains(node) === true);
1459
1616
  const handleClick = (e) => {
1460
- if (!wrapperRef.current?.contains(e.target)) {
1461
- setIsOpen(false);
1462
- }
1617
+ if (!isInside(e.target)) setIsOpen(false);
1463
1618
  };
1464
1619
  const handleKey = (e) => {
1465
1620
  if (e.key === "Escape") {
@@ -1467,18 +1622,100 @@ function MessageActionsMenu({
1467
1622
  triggerRef.current?.focus();
1468
1623
  }
1469
1624
  };
1625
+ const handleScroll = (e) => {
1626
+ if (!isInside(e.target)) setIsOpen(false);
1627
+ };
1628
+ const handleResize = () => setIsOpen(false);
1470
1629
  document.addEventListener("mousedown", handleClick);
1471
1630
  document.addEventListener("keydown", handleKey);
1631
+ window.addEventListener("resize", handleResize);
1632
+ window.addEventListener("scroll", handleScroll, true);
1472
1633
  return () => {
1473
1634
  document.removeEventListener("mousedown", handleClick);
1474
1635
  document.removeEventListener("keydown", handleKey);
1636
+ window.removeEventListener("resize", handleResize);
1637
+ window.removeEventListener("scroll", handleScroll, true);
1475
1638
  };
1476
1639
  }, [isOpen]);
1477
1640
  const run = (fn) => {
1478
1641
  setIsOpen(false);
1479
1642
  fn();
1480
1643
  };
1481
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrapperRef, className, style: styles4.wrapper, children: [
1644
+ const menu = isOpen ? /* @__PURE__ */ jsxRuntime.jsxs(
1645
+ "div",
1646
+ {
1647
+ ref: menuRef,
1648
+ role: "menu",
1649
+ "aria-label": "Message actions",
1650
+ style: {
1651
+ ...styles4.menu,
1652
+ // Render off-screen on first paint so we can measure before placing,
1653
+ // then snap to computed coords on the next layout pass.
1654
+ top: coords?.top ?? -9999,
1655
+ left: coords?.left ?? -9999,
1656
+ visibility: coords ? "visible" : "hidden"
1657
+ },
1658
+ children: [
1659
+ /* @__PURE__ */ jsxRuntime.jsxs(
1660
+ "button",
1661
+ {
1662
+ type: "button",
1663
+ role: "menuitem",
1664
+ onClick: () => run(() => onReply(message)),
1665
+ style: styles4.item,
1666
+ children: [
1667
+ /* @__PURE__ */ jsxRuntime.jsx(ReplyIcon, { size: 18, color: "#374151" }),
1668
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Reply" })
1669
+ ]
1670
+ }
1671
+ ),
1672
+ isPinned ? /* @__PURE__ */ jsxRuntime.jsxs(
1673
+ "button",
1674
+ {
1675
+ type: "button",
1676
+ role: "menuitem",
1677
+ onClick: () => run(() => onUnpin(message.id)),
1678
+ style: styles4.item,
1679
+ children: [
1680
+ /* @__PURE__ */ jsxRuntime.jsx(PinIcon, { size: 16, color: "#374151" }),
1681
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Unpin" })
1682
+ ]
1683
+ }
1684
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
1685
+ "button",
1686
+ {
1687
+ type: "button",
1688
+ role: "menuitem",
1689
+ onClick: () => run(() => onPin(message.id)),
1690
+ disabled: pinDisabled,
1691
+ style: {
1692
+ ...styles4.item,
1693
+ ...pinDisabled ? styles4.itemDisabled : {}
1694
+ },
1695
+ title: pinDisabled ? "Pin limit reached (max 3)" : void 0,
1696
+ children: [
1697
+ /* @__PURE__ */ jsxRuntime.jsx(PinIcon, { size: 16, color: "#374151" }),
1698
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: pinDisabled ? "Pin (limit reached)" : "Pin" })
1699
+ ]
1700
+ }
1701
+ ),
1702
+ canCopy && onCopy && /* @__PURE__ */ jsxRuntime.jsxs(
1703
+ "button",
1704
+ {
1705
+ type: "button",
1706
+ role: "menuitem",
1707
+ onClick: () => run(() => onCopy(message.body)),
1708
+ style: styles4.item,
1709
+ children: [
1710
+ /* @__PURE__ */ jsxRuntime.jsx(CopyIcon, { size: 16, color: "#374151" }),
1711
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Copy text" })
1712
+ ]
1713
+ }
1714
+ )
1715
+ ]
1716
+ }
1717
+ ) : null;
1718
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles4.wrapper, children: [
1482
1719
  /* @__PURE__ */ jsxRuntime.jsx(
1483
1720
  "button",
1484
1721
  {
@@ -1496,75 +1733,7 @@ function MessageActionsMenu({
1496
1733
  children: /* @__PURE__ */ jsxRuntime.jsx(MoreIcon, { size: 18, color: "#4b5563" })
1497
1734
  }
1498
1735
  ),
1499
- isOpen && /* @__PURE__ */ jsxRuntime.jsxs(
1500
- "div",
1501
- {
1502
- role: "menu",
1503
- "aria-label": "Message actions",
1504
- style: {
1505
- ...styles4.menu,
1506
- ...anchorRight ? styles4.menuRight : styles4.menuLeft
1507
- },
1508
- children: [
1509
- /* @__PURE__ */ jsxRuntime.jsxs(
1510
- "button",
1511
- {
1512
- type: "button",
1513
- role: "menuitem",
1514
- onClick: () => run(() => onReply(message)),
1515
- style: styles4.item,
1516
- children: [
1517
- /* @__PURE__ */ jsxRuntime.jsx(ReplyIcon, { size: 18, color: "#374151" }),
1518
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Reply" })
1519
- ]
1520
- }
1521
- ),
1522
- isPinned ? /* @__PURE__ */ jsxRuntime.jsxs(
1523
- "button",
1524
- {
1525
- type: "button",
1526
- role: "menuitem",
1527
- onClick: () => run(() => onUnpin(message.id)),
1528
- style: styles4.item,
1529
- children: [
1530
- /* @__PURE__ */ jsxRuntime.jsx(PinIcon, { size: 16, color: "#374151" }),
1531
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Unpin" })
1532
- ]
1533
- }
1534
- ) : /* @__PURE__ */ jsxRuntime.jsxs(
1535
- "button",
1536
- {
1537
- type: "button",
1538
- role: "menuitem",
1539
- onClick: () => run(() => onPin(message.id)),
1540
- disabled: pinDisabled,
1541
- style: {
1542
- ...styles4.item,
1543
- ...pinDisabled ? styles4.itemDisabled : {}
1544
- },
1545
- title: pinDisabled ? "Pin limit reached (max 3)" : void 0,
1546
- children: [
1547
- /* @__PURE__ */ jsxRuntime.jsx(PinIcon, { size: 16, color: "#374151" }),
1548
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: pinDisabled ? "Pin (limit reached)" : "Pin" })
1549
- ]
1550
- }
1551
- ),
1552
- canCopy && onCopy && /* @__PURE__ */ jsxRuntime.jsxs(
1553
- "button",
1554
- {
1555
- type: "button",
1556
- role: "menuitem",
1557
- onClick: () => run(() => onCopy(message.body)),
1558
- style: styles4.item,
1559
- children: [
1560
- /* @__PURE__ */ jsxRuntime.jsx(CopyIcon, { size: 16, color: "#374151" }),
1561
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Copy text" })
1562
- ]
1563
- }
1564
- )
1565
- ]
1566
- }
1567
- )
1736
+ menu && typeof document !== "undefined" ? reactDom.createPortal(menu, document.body) : null
1568
1737
  ] });
1569
1738
  }
1570
1739
  var styles4 = {
@@ -1585,22 +1754,15 @@ var styles4 = {
1585
1754
  color: "#4b5563"
1586
1755
  },
1587
1756
  menu: {
1588
- position: "absolute",
1589
- top: "100%",
1590
- marginTop: "4px",
1757
+ position: "fixed",
1591
1758
  minWidth: "160px",
1592
1759
  backgroundColor: "#ffffff",
1593
1760
  border: "1px solid #e5e7eb",
1594
1761
  borderRadius: "10px",
1595
1762
  boxShadow: "0 6px 18px rgba(0, 0, 0, 0.12)",
1596
1763
  padding: "4px",
1597
- zIndex: 20
1598
- },
1599
- menuLeft: {
1600
- left: 0
1601
- },
1602
- menuRight: {
1603
- right: 0
1764
+ // Above the floating popup (z-index 9999) but below any future modal.
1765
+ zIndex: 1e4
1604
1766
  },
1605
1767
  item: {
1606
1768
  display: "flex",
@@ -2253,31 +2415,34 @@ var MessageList = React4.forwardRef(function MessageList2({
2253
2415
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles6.emptySubtitle, children: "Start the conversation \u2014 everyone on this case will see what you write." })
2254
2416
  ] }),
2255
2417
  messages.length === 0 && isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles6.firstLoad, children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: 20 }) }),
2256
- messages.map((message, index) => {
2257
- const currentBucket = dayBucketKey(message.insertedAt);
2258
- const prevBucket = index === 0 ? null : dayBucketKey(messages[index - 1].insertedAt);
2259
- const showDivider = currentBucket && currentBucket !== prevBucket;
2260
- return /* @__PURE__ */ jsxRuntime.jsxs(React4__default.default.Fragment, { children: [
2261
- showDivider && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles6.dayDividerPill, children: formatDayDivider(message.insertedAt) }) }),
2262
- /* @__PURE__ */ jsxRuntime.jsx("div", { "data-message-id": message.id, children: /* @__PURE__ */ jsxRuntime.jsx(
2263
- MessageBubble,
2264
- {
2265
- message,
2266
- isOwn: message.senderId === currentUserId,
2267
- participants,
2268
- currentUserId,
2269
- showSeenBy,
2270
- onDeepLinkClick,
2271
- onReply,
2272
- onPin,
2273
- onUnpin,
2274
- onCopy,
2275
- onRetry,
2276
- pinDisabled
2277
- }
2278
- ) })
2279
- ] }, message.id);
2280
- }),
2418
+ (() => {
2419
+ let lastValidBucket = null;
2420
+ return messages.map((message) => {
2421
+ const currentBucket = dayBucketKey(message.insertedAt);
2422
+ const showDivider = !!currentBucket && currentBucket !== lastValidBucket;
2423
+ if (currentBucket) lastValidBucket = currentBucket;
2424
+ return /* @__PURE__ */ jsxRuntime.jsxs(React4__default.default.Fragment, { children: [
2425
+ showDivider && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles6.dayDividerPill, children: formatDayDivider(message.insertedAt) }) }),
2426
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-message-id": message.id, "data-natoe-message-bubble": true, style: styles6.bubbleSlot, children: /* @__PURE__ */ jsxRuntime.jsx(
2427
+ MessageBubble,
2428
+ {
2429
+ message,
2430
+ isOwn: message.senderId === currentUserId,
2431
+ participants,
2432
+ currentUserId,
2433
+ showSeenBy,
2434
+ onDeepLinkClick,
2435
+ onReply,
2436
+ onPin,
2437
+ onUnpin,
2438
+ onCopy,
2439
+ onRetry,
2440
+ pinDisabled
2441
+ }
2442
+ ) })
2443
+ ] }, message.id);
2444
+ });
2445
+ })(),
2281
2446
  typingUsers.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles6.typingIndicator, children: [
2282
2447
  typingUsers.map((t) => t.userName ?? "Someone").join(", "),
2283
2448
  typingUsers.length === 1 ? " is " : " are ",
@@ -2357,7 +2522,11 @@ var styles6 = {
2357
2522
  dayDivider: {
2358
2523
  display: "flex",
2359
2524
  justifyContent: "center",
2360
- margin: "12px 0 8px"
2525
+ margin: "12px 0 8px",
2526
+ position: "sticky",
2527
+ top: "4px",
2528
+ zIndex: 1,
2529
+ pointerEvents: "none"
2361
2530
  },
2362
2531
  dayDividerPill: {
2363
2532
  display: "inline-block",
@@ -2367,7 +2536,11 @@ var styles6 = {
2367
2536
  color: "#334155",
2368
2537
  backgroundColor: "#e2e8f0",
2369
2538
  borderRadius: "12px",
2370
- letterSpacing: "0.2px"
2539
+ letterSpacing: "0.2px",
2540
+ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.04)"
2541
+ },
2542
+ bubbleSlot: {
2543
+ animation: "natoe-colab-message-in 180ms ease-out"
2371
2544
  }
2372
2545
  };
2373
2546
  function ReplyPreview({ message, onCancel, className }) {
@@ -3419,11 +3592,52 @@ var styles12 = {
3419
3592
  letterSpacing: "0.4px"
3420
3593
  }
3421
3594
  };
3595
+
3596
+ // src/core/styles.ts
3597
+ var FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
3598
+ var ROOT_CLASS = "natoe-colab-root";
3599
+ var GLOBAL_STYLE_ID = "natoe-colab-global-styles";
3600
+ var GLOBAL_CSS = `
3601
+ @keyframes natoe-colab-spin { to { transform: rotate(360deg); } }
3602
+
3603
+ @keyframes natoe-colab-message-in {
3604
+ from { opacity: 0; transform: translateY(4px); }
3605
+ to { opacity: 1; transform: translateY(0); }
3606
+ }
3607
+
3608
+ /* High-contrast adjustments \u2014 older users on OS-level high-contrast mode
3609
+ get heavier borders and stronger text without losing the package look. */
3610
+ @media (prefers-contrast: more) {
3611
+ .${ROOT_CLASS} {
3612
+ color: #000000;
3613
+ }
3614
+ .${ROOT_CLASS} button {
3615
+ outline: 1px solid currentColor;
3616
+ }
3617
+ }
3618
+
3619
+ /* Honour reduced-motion preferences by killing entry animations. */
3620
+ @media (prefers-reduced-motion: reduce) {
3621
+ .${ROOT_CLASS} [data-natoe-message-bubble] {
3622
+ animation: none !important;
3623
+ }
3624
+ }
3625
+ `;
3626
+ function ensureGlobalStyles() {
3627
+ if (typeof document === "undefined") return;
3628
+ if (document.getElementById(GLOBAL_STYLE_ID)) return;
3629
+ const style = document.createElement("style");
3630
+ style.id = GLOBAL_STYLE_ID;
3631
+ style.textContent = GLOBAL_CSS;
3632
+ document.head.appendChild(style);
3633
+ }
3634
+ ensureGlobalStyles();
3422
3635
  function CollabPanel({
3423
3636
  orderId,
3424
3637
  patientData,
3425
3638
  participantIds,
3426
3639
  showSeenBy = true,
3640
+ onBack,
3427
3641
  className,
3428
3642
  style
3429
3643
  }) {
@@ -3497,13 +3711,13 @@ function CollabPanel({
3497
3711
  };
3498
3712
  const pinDisabled = pinnedMessages.length >= MAX_PINNED_MESSAGES;
3499
3713
  if (error) {
3500
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyles.errorState, children: [
3714
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyles.errorState, children: [
3501
3715
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: panelStyles.errorTitle, children: "Unable to load conversation" }),
3502
3716
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: panelStyles.errorMessage, children: error })
3503
3717
  ] }) });
3504
3718
  }
3505
3719
  if (isLoading && !conversation) {
3506
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
3720
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
3507
3721
  }
3508
3722
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...panelStyles.container, ...style }, children: [
3509
3723
  showSettings && conversation ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3531,7 +3745,8 @@ function CollabPanel({
3531
3745
  patientData,
3532
3746
  participants,
3533
3747
  onOpenDicom: handleOpenDicom,
3534
- onOpenSettings: () => setShowSettings(true)
3748
+ onOpenSettings: () => setShowSettings(true),
3749
+ onBack
3535
3750
  }
3536
3751
  ),
3537
3752
  pinnedMessages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
@@ -3589,7 +3804,7 @@ var panelStyles = {
3589
3804
  borderRadius: "8px",
3590
3805
  overflow: "hidden",
3591
3806
  border: "1px solid #e5e7eb",
3592
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
3807
+ fontFamily: FONT_STACK
3593
3808
  },
3594
3809
  loadingState: {
3595
3810
  display: "flex",
@@ -3648,6 +3863,7 @@ function ExpandIcon({ size = 18, color = "currentColor" }) {
3648
3863
  }
3649
3864
  );
3650
3865
  }
3866
+ ensureGlobalStyles();
3651
3867
  var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3652
3868
  function CollabPopup({
3653
3869
  orderId,
@@ -3655,6 +3871,7 @@ function CollabPopup({
3655
3871
  participantIds,
3656
3872
  isOpen,
3657
3873
  onClose,
3874
+ onBack,
3658
3875
  initialPosition,
3659
3876
  width = 380,
3660
3877
  height = 520,
@@ -3748,7 +3965,7 @@ function CollabPopup({
3748
3965
  "div",
3749
3966
  {
3750
3967
  ref: containerRef,
3751
- className,
3968
+ className: [ROOT_CLASS, className].filter(Boolean).join(" "),
3752
3969
  role: "dialog",
3753
3970
  "aria-modal": "true",
3754
3971
  "aria-labelledby": titleId,
@@ -3764,7 +3981,18 @@ function CollabPopup({
3764
3981
  onMouseDown: handleMouseDown,
3765
3982
  children: [
3766
3983
  /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-drag-handle": true, style: styles13.titleBar, children: [
3767
- /* @__PURE__ */ jsxRuntime.jsx("span", { id: titleId, style: styles13.titleText, children: buildChannelName(patientData) }),
3984
+ onBack && /* @__PURE__ */ jsxRuntime.jsx(
3985
+ "button",
3986
+ {
3987
+ onClick: onBack,
3988
+ style: styles13.titleButton,
3989
+ "aria-label": "Go back",
3990
+ title: "Back",
3991
+ type: "button",
3992
+ children: "\u2190"
3993
+ }
3994
+ ),
3995
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: titleId, style: { ...styles13.titleText, flex: 1 }, children: buildChannelName(patientData) }),
3768
3996
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleActions, children: [
3769
3997
  /* @__PURE__ */ jsxRuntime.jsx(
3770
3998
  "button",
@@ -3806,7 +4034,8 @@ var styles13 = {
3806
4034
  flexDirection: "column",
3807
4035
  backgroundColor: "#ffffff",
3808
4036
  border: "1px solid #e5e7eb",
3809
- transition: "height 0.2s ease"
4037
+ transition: "height 0.2s ease",
4038
+ fontFamily: FONT_STACK
3810
4039
  },
3811
4040
  titleBar: {
3812
4041
  display: "flex",
@@ -4051,8 +4280,8 @@ function useInlineCollab({
4051
4280
  await dispatchSend({
4052
4281
  body: "Voice message",
4053
4282
  type: "audio",
4054
- mediaUrl: url,
4055
- mediaDuration: duration
4283
+ media_url: url,
4284
+ media_duration: duration
4056
4285
  });
4057
4286
  } catch (err) {
4058
4287
  setError(err instanceof Error ? err.message : "Failed to send voice message");
@@ -4078,6 +4307,7 @@ function useInlineCollab({
4078
4307
  containerRef
4079
4308
  };
4080
4309
  }
4310
+ ensureGlobalStyles();
4081
4311
  function CollabInline({
4082
4312
  orderId,
4083
4313
  patientData,
@@ -4100,9 +4330,9 @@ function CollabInline({
4100
4330
  containerRef
4101
4331
  } = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
4102
4332
  if (isLoading && !hasConversation) {
4103
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...styles14.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4333
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4104
4334
  }
4105
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className, style: { ...styles14.container, ...style }, children: [
4335
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: [
4106
4336
  hasConversation && messages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles14.preview, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsxRuntime.jsx(InlineMessageRow, { message }, message.id)) }),
4107
4337
  error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles14.error, children: error }),
4108
4338
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4318,7 +4548,7 @@ var styles14 = {
4318
4548
  backgroundColor: "#ffffff",
4319
4549
  border: "1px solid #e5e7eb",
4320
4550
  borderRadius: "8px",
4321
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
4551
+ fontFamily: FONT_STACK
4322
4552
  },
4323
4553
  loadingState: {
4324
4554
  padding: "8px",
@@ -4948,6 +5178,8 @@ var styles16 = {
4948
5178
  textAlign: "center"
4949
5179
  }
4950
5180
  };
5181
+ ensureGlobalStyles();
5182
+ var COMPACT_BREAKPOINT = 720;
4951
5183
  function CollabInbox({
4952
5184
  initialConversationId,
4953
5185
  onSelectConversation,
@@ -4957,34 +5189,55 @@ function CollabInbox({
4957
5189
  }) {
4958
5190
  const { conversations, isLoading, error } = useConversationList();
4959
5191
  const [selectedId, setSelectedId] = React4.useState(initialConversationId ?? null);
5192
+ const [isCompact, setIsCompact] = React4.useState(
5193
+ () => typeof window === "undefined" ? false : window.innerWidth < COMPACT_BREAKPOINT
5194
+ );
5195
+ React4.useEffect(() => {
5196
+ if (typeof window === "undefined") return;
5197
+ const mq = window.matchMedia(`(max-width: ${COMPACT_BREAKPOINT - 1}px)`);
5198
+ const sync = () => setIsCompact(mq.matches);
5199
+ sync();
5200
+ mq.addEventListener("change", sync);
5201
+ return () => mq.removeEventListener("change", sync);
5202
+ }, []);
4960
5203
  const selected = conversations.find((c) => c.id === selectedId) || null;
4961
5204
  const handleSelect = (item) => {
4962
5205
  setSelectedId(item.id);
4963
5206
  onSelectConversation?.(item);
4964
5207
  };
4965
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...styles17.container, ...style }, children: [
4966
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles17.sidebar, children: /* @__PURE__ */ jsxRuntime.jsx(
4967
- ConversationList,
4968
- {
4969
- conversations,
4970
- selectedId,
4971
- isLoading,
4972
- error,
4973
- onSelect: handleSelect,
4974
- title
4975
- }
4976
- ) }),
4977
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles17.main, children: selected ? /* @__PURE__ */ jsxRuntime.jsx(
4978
- CollabPanel,
4979
- {
4980
- orderId: selected.orderId,
4981
- patientData: buildPatientData(selected),
4982
- showSeenBy: true,
4983
- style: styles17.panel
4984
- },
4985
- selected.id
4986
- ) : /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { hasAny: conversations.length > 0 }) })
4987
- ] });
5208
+ const showDetail = !isCompact || isCompact && !!selected;
5209
+ const showList = !isCompact || isCompact && !selected;
5210
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5211
+ "div",
5212
+ {
5213
+ className: [ROOT_CLASS, className].filter(Boolean).join(" "),
5214
+ style: { ...styles17.container, ...style },
5215
+ children: [
5216
+ showList && /* @__PURE__ */ jsxRuntime.jsx("div", { style: isCompact ? styles17.sidebarCompact : styles17.sidebar, children: /* @__PURE__ */ jsxRuntime.jsx(
5217
+ ConversationList,
5218
+ {
5219
+ conversations,
5220
+ selectedId,
5221
+ isLoading,
5222
+ error,
5223
+ onSelect: handleSelect,
5224
+ title
5225
+ }
5226
+ ) }),
5227
+ showDetail && /* @__PURE__ */ jsxRuntime.jsx("div", { style: isCompact ? styles17.mainCompact : styles17.main, children: selected ? /* @__PURE__ */ jsxRuntime.jsx(
5228
+ CollabPanel,
5229
+ {
5230
+ orderId: selected.orderId,
5231
+ patientData: buildPatientData(selected),
5232
+ showSeenBy: true,
5233
+ style: styles17.panel,
5234
+ onBack: isCompact ? () => setSelectedId(null) : void 0
5235
+ },
5236
+ selected.id
5237
+ ) : /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { hasAny: conversations.length > 0 }) })
5238
+ ]
5239
+ }
5240
+ );
4988
5241
  }
4989
5242
  function buildPatientData(item) {
4990
5243
  if (item.patientSnapshot) return item.patientSnapshot;
@@ -5006,7 +5259,7 @@ var styles17 = {
5006
5259
  height: "100%",
5007
5260
  width: "100%",
5008
5261
  backgroundColor: "#ffffff",
5009
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
5262
+ fontFamily: FONT_STACK
5010
5263
  },
5011
5264
  sidebar: {
5012
5265
  width: "320px",
@@ -5015,6 +5268,12 @@ var styles17 = {
5015
5268
  display: "flex",
5016
5269
  flexDirection: "column"
5017
5270
  },
5271
+ sidebarCompact: {
5272
+ width: "100%",
5273
+ flex: 1,
5274
+ display: "flex",
5275
+ flexDirection: "column"
5276
+ },
5018
5277
  main: {
5019
5278
  flex: 1,
5020
5279
  display: "flex",
@@ -5023,6 +5282,13 @@ var styles17 = {
5023
5282
  padding: "12px",
5024
5283
  backgroundColor: "#f3f4f6"
5025
5284
  },
5285
+ mainCompact: {
5286
+ flex: 1,
5287
+ display: "flex",
5288
+ flexDirection: "column",
5289
+ minWidth: 0,
5290
+ backgroundColor: "#f3f4f6"
5291
+ },
5026
5292
  panel: {
5027
5293
  height: "100%",
5028
5294
  width: "100%"