@natoe/colab 0.1.6 → 0.1.8

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
 
@@ -1150,71 +1151,190 @@ function useDeepLinks() {
1150
1151
  function escapeRegex(string) {
1151
1152
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1152
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
+ }
1153
1205
  function PatientHeader({
1154
1206
  patientData,
1155
1207
  onOpenDicom,
1156
1208
  onOpenSettings,
1209
+ onBack,
1157
1210
  className
1158
1211
  }) {
1159
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
+ }
1160
1223
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles.container, children: [
1161
- /* @__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) }) }) }),
1162
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.secondaryRow, children: [
1163
- patientData.patientAge && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.secondary, children: [
1164
- patientData.patientAge,
1165
- patientData.patientSex ? `/${patientData.patientSex}` : ""
1166
- ] }),
1167
- patientData.studyType && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.secondary, children: patientData.studyType }),
1168
- patientData.bodyParts && patientData.bodyParts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.secondary, children: patientData.bodyParts.join(", ") }),
1169
- patientData.referringPhysician && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.secondary, children: [
1170
- "Ref: ",
1171
- patientData.referringPhysician
1172
- ] })
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) })
1173
1237
  ] }),
1174
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actions, children: [
1175
- hasDicom && onOpenDicom && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onOpenDicom, style: styles.dicomButton, type: "button", children: "View DICOM" }),
1176
- onOpenSettings && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onOpenSettings, style: styles.settingsButton, type: "button", children: "Settings" })
1177
- ] })
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
1178
1274
  ] });
1179
1275
  }
1180
1276
  var styles = {
1181
1277
  container: {
1182
- padding: "12px 16px",
1278
+ padding: "14px 16px",
1183
1279
  borderBottom: "1px solid #e5e7eb",
1184
1280
  backgroundColor: "#f9fafb"
1185
1281
  },
1186
- infoRow: {
1282
+ nameRow: {
1187
1283
  display: "flex",
1188
- justifyContent: "space-between",
1189
- alignItems: "center"
1284
+ alignItems: "center",
1285
+ gap: "6px"
1190
1286
  },
1191
- patientInfo: {
1287
+ backButton: {
1288
+ width: "36px",
1289
+ height: "36px",
1192
1290
  display: "flex",
1193
1291
  alignItems: "center",
1194
- gap: "8px",
1195
- flexWrap: "wrap"
1292
+ justifyContent: "center",
1293
+ marginLeft: "-8px",
1294
+ backgroundColor: "transparent",
1295
+ border: "none",
1296
+ borderRadius: "50%",
1297
+ cursor: "pointer",
1298
+ flexShrink: 0
1196
1299
  },
1197
- patientName: {
1300
+ nameText: {
1301
+ fontSize: "18px",
1198
1302
  fontWeight: 600,
1199
- fontSize: "17px",
1200
- color: "#111827"
1303
+ color: "#111827",
1304
+ lineHeight: 1.3,
1305
+ minWidth: 0,
1306
+ overflow: "hidden",
1307
+ textOverflow: "ellipsis",
1308
+ whiteSpace: "nowrap"
1201
1309
  },
1202
- secondaryRow: {
1310
+ metaRow: {
1203
1311
  display: "flex",
1204
- gap: "12px",
1205
- marginTop: "4px",
1206
- alignItems: "center"
1312
+ flexWrap: "wrap",
1313
+ columnGap: "14px",
1314
+ rowGap: "4px",
1315
+ marginTop: "6px"
1207
1316
  },
1208
- secondary: {
1317
+ metaItem: {
1209
1318
  fontSize: "13px",
1210
- color: "#4b5563"
1319
+ color: "#374151",
1320
+ whiteSpace: "nowrap"
1321
+ },
1322
+ metaLabel: {
1323
+ color: "#6b7280",
1324
+ fontWeight: 500
1325
+ },
1326
+ metaValue: {
1327
+ color: "#111827"
1211
1328
  },
1212
1329
  actions: {
1213
1330
  display: "flex",
1214
1331
  gap: "8px",
1215
- marginTop: "8px"
1332
+ marginTop: "10px"
1216
1333
  },
1217
1334
  dicomButton: {
1335
+ display: "inline-flex",
1336
+ alignItems: "center",
1337
+ gap: "8px",
1218
1338
  minHeight: "40px",
1219
1339
  padding: "8px 16px",
1220
1340
  fontSize: "14px",
@@ -1226,6 +1346,9 @@ var styles = {
1226
1346
  cursor: "pointer"
1227
1347
  },
1228
1348
  settingsButton: {
1349
+ display: "inline-flex",
1350
+ alignItems: "center",
1351
+ gap: "8px",
1229
1352
  minHeight: "40px",
1230
1353
  padding: "8px 16px",
1231
1354
  fontSize: "14px",
@@ -1442,6 +1565,8 @@ function MoreIcon({ size = 18, color = "currentColor" }) {
1442
1565
  }
1443
1566
  );
1444
1567
  }
1568
+ var VIEWPORT_MARGIN = 8;
1569
+ var TRIGGER_GAP = 4;
1445
1570
  function MessageActionsMenu({
1446
1571
  message,
1447
1572
  onReply,
@@ -1453,16 +1578,43 @@ function MessageActionsMenu({
1453
1578
  className
1454
1579
  }) {
1455
1580
  const [isOpen, setIsOpen] = React4.useState(false);
1456
- const wrapperRef = React4.useRef(null);
1581
+ const [coords, setCoords] = React4.useState(null);
1457
1582
  const triggerRef = React4.useRef(null);
1583
+ const menuRef = React4.useRef(null);
1458
1584
  const isPinned = !!message.isPinned;
1459
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]);
1460
1613
  React4.useEffect(() => {
1461
1614
  if (!isOpen) return;
1615
+ const isInside = (node) => !!node && (triggerRef.current?.contains(node) === true || menuRef.current?.contains(node) === true);
1462
1616
  const handleClick = (e) => {
1463
- if (!wrapperRef.current?.contains(e.target)) {
1464
- setIsOpen(false);
1465
- }
1617
+ if (!isInside(e.target)) setIsOpen(false);
1466
1618
  };
1467
1619
  const handleKey = (e) => {
1468
1620
  if (e.key === "Escape") {
@@ -1470,18 +1622,100 @@ function MessageActionsMenu({
1470
1622
  triggerRef.current?.focus();
1471
1623
  }
1472
1624
  };
1625
+ const handleScroll = (e) => {
1626
+ if (!isInside(e.target)) setIsOpen(false);
1627
+ };
1628
+ const handleResize = () => setIsOpen(false);
1473
1629
  document.addEventListener("mousedown", handleClick);
1474
1630
  document.addEventListener("keydown", handleKey);
1631
+ window.addEventListener("resize", handleResize);
1632
+ window.addEventListener("scroll", handleScroll, true);
1475
1633
  return () => {
1476
1634
  document.removeEventListener("mousedown", handleClick);
1477
1635
  document.removeEventListener("keydown", handleKey);
1636
+ window.removeEventListener("resize", handleResize);
1637
+ window.removeEventListener("scroll", handleScroll, true);
1478
1638
  };
1479
1639
  }, [isOpen]);
1480
1640
  const run = (fn) => {
1481
1641
  setIsOpen(false);
1482
1642
  fn();
1483
1643
  };
1484
- 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: [
1485
1719
  /* @__PURE__ */ jsxRuntime.jsx(
1486
1720
  "button",
1487
1721
  {
@@ -1499,75 +1733,7 @@ function MessageActionsMenu({
1499
1733
  children: /* @__PURE__ */ jsxRuntime.jsx(MoreIcon, { size: 18, color: "#4b5563" })
1500
1734
  }
1501
1735
  ),
1502
- isOpen && /* @__PURE__ */ jsxRuntime.jsxs(
1503
- "div",
1504
- {
1505
- role: "menu",
1506
- "aria-label": "Message actions",
1507
- style: {
1508
- ...styles4.menu,
1509
- ...anchorRight ? styles4.menuRight : styles4.menuLeft
1510
- },
1511
- children: [
1512
- /* @__PURE__ */ jsxRuntime.jsxs(
1513
- "button",
1514
- {
1515
- type: "button",
1516
- role: "menuitem",
1517
- onClick: () => run(() => onReply(message)),
1518
- style: styles4.item,
1519
- children: [
1520
- /* @__PURE__ */ jsxRuntime.jsx(ReplyIcon, { size: 18, color: "#374151" }),
1521
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Reply" })
1522
- ]
1523
- }
1524
- ),
1525
- isPinned ? /* @__PURE__ */ jsxRuntime.jsxs(
1526
- "button",
1527
- {
1528
- type: "button",
1529
- role: "menuitem",
1530
- onClick: () => run(() => onUnpin(message.id)),
1531
- style: styles4.item,
1532
- children: [
1533
- /* @__PURE__ */ jsxRuntime.jsx(PinIcon, { size: 16, color: "#374151" }),
1534
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Unpin" })
1535
- ]
1536
- }
1537
- ) : /* @__PURE__ */ jsxRuntime.jsxs(
1538
- "button",
1539
- {
1540
- type: "button",
1541
- role: "menuitem",
1542
- onClick: () => run(() => onPin(message.id)),
1543
- disabled: pinDisabled,
1544
- style: {
1545
- ...styles4.item,
1546
- ...pinDisabled ? styles4.itemDisabled : {}
1547
- },
1548
- title: pinDisabled ? "Pin limit reached (max 3)" : void 0,
1549
- children: [
1550
- /* @__PURE__ */ jsxRuntime.jsx(PinIcon, { size: 16, color: "#374151" }),
1551
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: pinDisabled ? "Pin (limit reached)" : "Pin" })
1552
- ]
1553
- }
1554
- ),
1555
- canCopy && onCopy && /* @__PURE__ */ jsxRuntime.jsxs(
1556
- "button",
1557
- {
1558
- type: "button",
1559
- role: "menuitem",
1560
- onClick: () => run(() => onCopy(message.body)),
1561
- style: styles4.item,
1562
- children: [
1563
- /* @__PURE__ */ jsxRuntime.jsx(CopyIcon, { size: 16, color: "#374151" }),
1564
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Copy text" })
1565
- ]
1566
- }
1567
- )
1568
- ]
1569
- }
1570
- )
1736
+ menu && typeof document !== "undefined" ? reactDom.createPortal(menu, document.body) : null
1571
1737
  ] });
1572
1738
  }
1573
1739
  var styles4 = {
@@ -1588,22 +1754,15 @@ var styles4 = {
1588
1754
  color: "#4b5563"
1589
1755
  },
1590
1756
  menu: {
1591
- position: "absolute",
1592
- top: "100%",
1593
- marginTop: "4px",
1757
+ position: "fixed",
1594
1758
  minWidth: "160px",
1595
1759
  backgroundColor: "#ffffff",
1596
1760
  border: "1px solid #e5e7eb",
1597
1761
  borderRadius: "10px",
1598
1762
  boxShadow: "0 6px 18px rgba(0, 0, 0, 0.12)",
1599
1763
  padding: "4px",
1600
- zIndex: 20
1601
- },
1602
- menuLeft: {
1603
- left: 0
1604
- },
1605
- menuRight: {
1606
- right: 0
1764
+ // Above the floating popup (z-index 9999) but below any future modal.
1765
+ zIndex: 1e4
1607
1766
  },
1608
1767
  item: {
1609
1768
  display: "flex",
@@ -2256,31 +2415,34 @@ var MessageList = React4.forwardRef(function MessageList2({
2256
2415
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: styles6.emptySubtitle, children: "Start the conversation \u2014 everyone on this case will see what you write." })
2257
2416
  ] }),
2258
2417
  messages.length === 0 && isLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles6.firstLoad, children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { size: 20 }) }),
2259
- messages.map((message, index) => {
2260
- const currentBucket = dayBucketKey(message.insertedAt);
2261
- const prevBucket = index === 0 ? null : dayBucketKey(messages[index - 1].insertedAt);
2262
- const showDivider = currentBucket && currentBucket !== prevBucket;
2263
- return /* @__PURE__ */ jsxRuntime.jsxs(React4__default.default.Fragment, { children: [
2264
- 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) }) }),
2265
- /* @__PURE__ */ jsxRuntime.jsx("div", { "data-message-id": message.id, children: /* @__PURE__ */ jsxRuntime.jsx(
2266
- MessageBubble,
2267
- {
2268
- message,
2269
- isOwn: message.senderId === currentUserId,
2270
- participants,
2271
- currentUserId,
2272
- showSeenBy,
2273
- onDeepLinkClick,
2274
- onReply,
2275
- onPin,
2276
- onUnpin,
2277
- onCopy,
2278
- onRetry,
2279
- pinDisabled
2280
- }
2281
- ) })
2282
- ] }, message.id);
2283
- }),
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
+ })(),
2284
2446
  typingUsers.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles6.typingIndicator, children: [
2285
2447
  typingUsers.map((t) => t.userName ?? "Someone").join(", "),
2286
2448
  typingUsers.length === 1 ? " is " : " are ",
@@ -2360,7 +2522,11 @@ var styles6 = {
2360
2522
  dayDivider: {
2361
2523
  display: "flex",
2362
2524
  justifyContent: "center",
2363
- margin: "12px 0 8px"
2525
+ margin: "12px 0 8px",
2526
+ position: "sticky",
2527
+ top: "4px",
2528
+ zIndex: 1,
2529
+ pointerEvents: "none"
2364
2530
  },
2365
2531
  dayDividerPill: {
2366
2532
  display: "inline-block",
@@ -2370,7 +2536,11 @@ var styles6 = {
2370
2536
  color: "#334155",
2371
2537
  backgroundColor: "#e2e8f0",
2372
2538
  borderRadius: "12px",
2373
- 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"
2374
2544
  }
2375
2545
  };
2376
2546
  function ReplyPreview({ message, onCancel, className }) {
@@ -2521,6 +2691,7 @@ function MessageInput({
2521
2691
  }) {
2522
2692
  const [text, setText] = React4.useState("");
2523
2693
  const [isSending, setIsSending] = React4.useState(false);
2694
+ const [isUploading, setIsUploading] = React4.useState(false);
2524
2695
  const [fileError, setFileError] = React4.useState(null);
2525
2696
  const fileInputRef = React4.useRef(null);
2526
2697
  const typingTimeoutRef = React4.useRef(null);
@@ -2584,10 +2755,12 @@ function MessageInput({
2584
2755
  return;
2585
2756
  }
2586
2757
  setIsSending(true);
2758
+ setIsUploading(true);
2587
2759
  try {
2588
2760
  await onSendFile(file);
2589
2761
  } finally {
2590
2762
  setIsSending(false);
2763
+ setIsUploading(false);
2591
2764
  if (fileInputRef.current) fileInputRef.current.value = "";
2592
2765
  }
2593
2766
  },
@@ -2595,6 +2768,10 @@ function MessageInput({
2595
2768
  );
2596
2769
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles8.container, children: [
2597
2770
  replyTo && onCancelReply && /* @__PURE__ */ jsxRuntime.jsx(ReplyPreview, { message: replyTo, onCancel: onCancelReply }),
2771
+ isUploading && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.uploadBanner, children: [
2772
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles8.uploadSpinner }),
2773
+ "Uploading file\u2026"
2774
+ ] }),
2598
2775
  fileError && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles8.error, role: "alert", children: [
2599
2776
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles8.errorText, children: fileError }),
2600
2777
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2680,6 +2857,26 @@ var styles8 = {
2680
2857
  borderTop: "1px solid #e5e7eb",
2681
2858
  backgroundColor: "#ffffff"
2682
2859
  },
2860
+ uploadBanner: {
2861
+ display: "flex",
2862
+ alignItems: "center",
2863
+ gap: "8px",
2864
+ padding: "6px 14px",
2865
+ fontSize: "12px",
2866
+ color: "#6b7280",
2867
+ backgroundColor: "#f9fafb",
2868
+ borderBottom: "1px solid #e5e7eb"
2869
+ },
2870
+ uploadSpinner: {
2871
+ display: "inline-block",
2872
+ width: "12px",
2873
+ height: "12px",
2874
+ border: "2px solid #d1d5db",
2875
+ borderTopColor: "#7c3aed",
2876
+ borderRadius: "50%",
2877
+ animation: "natoe-colab-spin 0.7s linear infinite",
2878
+ flexShrink: 0
2879
+ },
2683
2880
  error: {
2684
2881
  display: "flex",
2685
2882
  alignItems: "center",
@@ -3422,11 +3619,52 @@ var styles12 = {
3422
3619
  letterSpacing: "0.4px"
3423
3620
  }
3424
3621
  };
3622
+
3623
+ // src/core/styles.ts
3624
+ var FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
3625
+ var ROOT_CLASS = "natoe-colab-root";
3626
+ var GLOBAL_STYLE_ID = "natoe-colab-global-styles";
3627
+ var GLOBAL_CSS = `
3628
+ @keyframes natoe-colab-spin { to { transform: rotate(360deg); } }
3629
+
3630
+ @keyframes natoe-colab-message-in {
3631
+ from { opacity: 0; transform: translateY(4px); }
3632
+ to { opacity: 1; transform: translateY(0); }
3633
+ }
3634
+
3635
+ /* High-contrast adjustments \u2014 older users on OS-level high-contrast mode
3636
+ get heavier borders and stronger text without losing the package look. */
3637
+ @media (prefers-contrast: more) {
3638
+ .${ROOT_CLASS} {
3639
+ color: #000000;
3640
+ }
3641
+ .${ROOT_CLASS} button {
3642
+ outline: 1px solid currentColor;
3643
+ }
3644
+ }
3645
+
3646
+ /* Honour reduced-motion preferences by killing entry animations. */
3647
+ @media (prefers-reduced-motion: reduce) {
3648
+ .${ROOT_CLASS} [data-natoe-message-bubble] {
3649
+ animation: none !important;
3650
+ }
3651
+ }
3652
+ `;
3653
+ function ensureGlobalStyles() {
3654
+ if (typeof document === "undefined") return;
3655
+ if (document.getElementById(GLOBAL_STYLE_ID)) return;
3656
+ const style = document.createElement("style");
3657
+ style.id = GLOBAL_STYLE_ID;
3658
+ style.textContent = GLOBAL_CSS;
3659
+ document.head.appendChild(style);
3660
+ }
3661
+ ensureGlobalStyles();
3425
3662
  function CollabPanel({
3426
3663
  orderId,
3427
3664
  patientData,
3428
3665
  participantIds,
3429
3666
  showSeenBy = true,
3667
+ onBack,
3430
3668
  className,
3431
3669
  style
3432
3670
  }) {
@@ -3500,13 +3738,13 @@ function CollabPanel({
3500
3738
  };
3501
3739
  const pinDisabled = pinnedMessages.length >= MAX_PINNED_MESSAGES;
3502
3740
  if (error) {
3503
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: panelStyles.errorState, children: [
3741
+ 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: [
3504
3742
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: panelStyles.errorTitle, children: "Unable to load conversation" }),
3505
3743
  /* @__PURE__ */ jsxRuntime.jsx("p", { style: panelStyles.errorMessage, children: error })
3506
3744
  ] }) });
3507
3745
  }
3508
3746
  if (isLoading && !conversation) {
3509
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
3747
+ 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..." }) });
3510
3748
  }
3511
3749
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...panelStyles.container, ...style }, children: [
3512
3750
  showSettings && conversation ? /* @__PURE__ */ jsxRuntime.jsx(
@@ -3534,7 +3772,8 @@ function CollabPanel({
3534
3772
  patientData,
3535
3773
  participants,
3536
3774
  onOpenDicom: handleOpenDicom,
3537
- onOpenSettings: () => setShowSettings(true)
3775
+ onOpenSettings: () => setShowSettings(true),
3776
+ onBack
3538
3777
  }
3539
3778
  ),
3540
3779
  pinnedMessages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
@@ -3592,7 +3831,7 @@ var panelStyles = {
3592
3831
  borderRadius: "8px",
3593
3832
  overflow: "hidden",
3594
3833
  border: "1px solid #e5e7eb",
3595
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
3834
+ fontFamily: FONT_STACK
3596
3835
  },
3597
3836
  loadingState: {
3598
3837
  display: "flex",
@@ -3651,6 +3890,7 @@ function ExpandIcon({ size = 18, color = "currentColor" }) {
3651
3890
  }
3652
3891
  );
3653
3892
  }
3893
+ ensureGlobalStyles();
3654
3894
  var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3655
3895
  function CollabPopup({
3656
3896
  orderId,
@@ -3752,7 +3992,7 @@ function CollabPopup({
3752
3992
  "div",
3753
3993
  {
3754
3994
  ref: containerRef,
3755
- className,
3995
+ className: [ROOT_CLASS, className].filter(Boolean).join(" "),
3756
3996
  role: "dialog",
3757
3997
  "aria-modal": "true",
3758
3998
  "aria-labelledby": titleId,
@@ -3821,7 +4061,8 @@ var styles13 = {
3821
4061
  flexDirection: "column",
3822
4062
  backgroundColor: "#ffffff",
3823
4063
  border: "1px solid #e5e7eb",
3824
- transition: "height 0.2s ease"
4064
+ transition: "height 0.2s ease",
4065
+ fontFamily: FONT_STACK
3825
4066
  },
3826
4067
  titleBar: {
3827
4068
  display: "flex",
@@ -4093,6 +4334,7 @@ function useInlineCollab({
4093
4334
  containerRef
4094
4335
  };
4095
4336
  }
4337
+ ensureGlobalStyles();
4096
4338
  function CollabInline({
4097
4339
  orderId,
4098
4340
  patientData,
@@ -4115,9 +4357,9 @@ function CollabInline({
4115
4357
  containerRef
4116
4358
  } = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
4117
4359
  if (isLoading && !hasConversation) {
4118
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: { ...styles14.container, ...style }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4360
+ 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..." }) });
4119
4361
  }
4120
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className, style: { ...styles14.container, ...style }, children: [
4362
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: [
4121
4363
  hasConversation && messages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles14.preview, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsxRuntime.jsx(InlineMessageRow, { message }, message.id)) }),
4122
4364
  error && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles14.error, children: error }),
4123
4365
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4333,7 +4575,7 @@ var styles14 = {
4333
4575
  backgroundColor: "#ffffff",
4334
4576
  border: "1px solid #e5e7eb",
4335
4577
  borderRadius: "8px",
4336
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
4578
+ fontFamily: FONT_STACK
4337
4579
  },
4338
4580
  loadingState: {
4339
4581
  padding: "8px",
@@ -4963,6 +5205,8 @@ var styles16 = {
4963
5205
  textAlign: "center"
4964
5206
  }
4965
5207
  };
5208
+ ensureGlobalStyles();
5209
+ var COMPACT_BREAKPOINT = 720;
4966
5210
  function CollabInbox({
4967
5211
  initialConversationId,
4968
5212
  onSelectConversation,
@@ -4972,37 +5216,70 @@ function CollabInbox({
4972
5216
  }) {
4973
5217
  const { conversations, isLoading, error } = useConversationList();
4974
5218
  const [selectedId, setSelectedId] = React4.useState(initialConversationId ?? null);
5219
+ const [isCompact, setIsCompact] = React4.useState(
5220
+ () => typeof window === "undefined" ? false : window.innerWidth < COMPACT_BREAKPOINT
5221
+ );
5222
+ React4.useEffect(() => {
5223
+ if (typeof window === "undefined") return;
5224
+ const mq = window.matchMedia(`(max-width: ${COMPACT_BREAKPOINT - 1}px)`);
5225
+ const sync = () => setIsCompact(mq.matches);
5226
+ sync();
5227
+ mq.addEventListener("change", sync);
5228
+ return () => mq.removeEventListener("change", sync);
5229
+ }, []);
4975
5230
  const selected = conversations.find((c) => c.id === selectedId) || null;
4976
5231
  const handleSelect = (item) => {
4977
5232
  setSelectedId(item.id);
4978
5233
  onSelectConversation?.(item);
4979
5234
  };
4980
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...styles17.container, ...style }, children: [
4981
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles17.sidebar, children: /* @__PURE__ */ jsxRuntime.jsx(
4982
- ConversationList,
4983
- {
4984
- conversations,
4985
- selectedId,
4986
- isLoading,
4987
- error,
4988
- onSelect: handleSelect,
4989
- title
4990
- }
4991
- ) }),
4992
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles17.main, children: selected ? /* @__PURE__ */ jsxRuntime.jsx(
4993
- CollabPanel,
4994
- {
4995
- orderId: selected.orderId,
4996
- patientData: buildPatientData(selected),
4997
- showSeenBy: true,
4998
- style: styles17.panel
4999
- },
5000
- selected.id
5001
- ) : /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { hasAny: conversations.length > 0 }) })
5002
- ] });
5235
+ const showDetail = !isCompact || isCompact && !!selected;
5236
+ const showList = !isCompact || isCompact && !selected;
5237
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5238
+ "div",
5239
+ {
5240
+ className: [ROOT_CLASS, className].filter(Boolean).join(" "),
5241
+ style: { ...styles17.container, ...style },
5242
+ children: [
5243
+ showList && /* @__PURE__ */ jsxRuntime.jsx("div", { style: isCompact ? styles17.sidebarCompact : styles17.sidebar, children: /* @__PURE__ */ jsxRuntime.jsx(
5244
+ ConversationList,
5245
+ {
5246
+ conversations,
5247
+ selectedId,
5248
+ isLoading,
5249
+ error,
5250
+ onSelect: handleSelect,
5251
+ title
5252
+ }
5253
+ ) }),
5254
+ showDetail && /* @__PURE__ */ jsxRuntime.jsx("div", { style: isCompact ? styles17.mainCompact : styles17.main, children: selected ? /* @__PURE__ */ jsxRuntime.jsx(
5255
+ CollabPanel,
5256
+ {
5257
+ orderId: selected.orderId,
5258
+ patientData: buildPatientData(selected),
5259
+ showSeenBy: true,
5260
+ style: styles17.panel,
5261
+ onBack: isCompact ? () => setSelectedId(null) : void 0
5262
+ },
5263
+ selected.id
5264
+ ) : /* @__PURE__ */ jsxRuntime.jsx(EmptyState, { hasAny: conversations.length > 0 }) })
5265
+ ]
5266
+ }
5267
+ );
5003
5268
  }
5004
5269
  function buildPatientData(item) {
5005
5270
  if (item.patientSnapshot) return item.patientSnapshot;
5271
+ const parts = item.name.split(" - ");
5272
+ if (parts.length >= 3) {
5273
+ const last4 = parts[parts.length - 1];
5274
+ const labName = parts[parts.length - 2];
5275
+ const patientName = parts.slice(0, parts.length - 2).join(" - ");
5276
+ return {
5277
+ patientName,
5278
+ labName: labName || void 0,
5279
+ orderId: item.orderId,
5280
+ displayOrderId: last4
5281
+ };
5282
+ }
5006
5283
  return {
5007
5284
  patientName: item.name,
5008
5285
  orderId: item.orderId
@@ -5021,7 +5298,7 @@ var styles17 = {
5021
5298
  height: "100%",
5022
5299
  width: "100%",
5023
5300
  backgroundColor: "#ffffff",
5024
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
5301
+ fontFamily: FONT_STACK
5025
5302
  },
5026
5303
  sidebar: {
5027
5304
  width: "320px",
@@ -5030,6 +5307,12 @@ var styles17 = {
5030
5307
  display: "flex",
5031
5308
  flexDirection: "column"
5032
5309
  },
5310
+ sidebarCompact: {
5311
+ width: "100%",
5312
+ flex: 1,
5313
+ display: "flex",
5314
+ flexDirection: "column"
5315
+ },
5033
5316
  main: {
5034
5317
  flex: 1,
5035
5318
  display: "flex",
@@ -5038,6 +5321,13 @@ var styles17 = {
5038
5321
  padding: "12px",
5039
5322
  backgroundColor: "#f3f4f6"
5040
5323
  },
5324
+ mainCompact: {
5325
+ flex: 1,
5326
+ display: "flex",
5327
+ flexDirection: "column",
5328
+ minWidth: 0,
5329
+ backgroundColor: "#f3f4f6"
5330
+ },
5041
5331
  panel: {
5042
5332
  height: "100%",
5043
5333
  width: "100%"