@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.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
 
@@ -1144,71 +1145,190 @@ function useDeepLinks() {
1144
1145
  function escapeRegex(string) {
1145
1146
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1146
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
+ }
1147
1199
  function PatientHeader({
1148
1200
  patientData,
1149
1201
  onOpenDicom,
1150
1202
  onOpenSettings,
1203
+ onBack,
1151
1204
  className
1152
1205
  }) {
1153
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
+ }
1154
1217
  return /* @__PURE__ */ jsxs("div", { className, style: styles.container, children: [
1155
- /* @__PURE__ */ jsx("div", { style: styles.infoRow, children: /* @__PURE__ */ jsx("div", { style: styles.patientInfo, children: /* @__PURE__ */ jsx("span", { style: styles.patientName, children: buildChannelName(patientData) }) }) }),
1156
- /* @__PURE__ */ jsxs("div", { style: styles.secondaryRow, children: [
1157
- patientData.patientAge && /* @__PURE__ */ jsxs("span", { style: styles.secondary, children: [
1158
- patientData.patientAge,
1159
- patientData.patientSex ? `/${patientData.patientSex}` : ""
1160
- ] }),
1161
- patientData.studyType && /* @__PURE__ */ jsx("span", { style: styles.secondary, children: patientData.studyType }),
1162
- patientData.bodyParts && patientData.bodyParts.length > 0 && /* @__PURE__ */ jsx("span", { style: styles.secondary, children: patientData.bodyParts.join(", ") }),
1163
- patientData.referringPhysician && /* @__PURE__ */ jsxs("span", { style: styles.secondary, children: [
1164
- "Ref: ",
1165
- patientData.referringPhysician
1166
- ] })
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) })
1167
1231
  ] }),
1168
- /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
1169
- hasDicom && onOpenDicom && /* @__PURE__ */ jsx("button", { onClick: onOpenDicom, style: styles.dicomButton, type: "button", children: "View DICOM" }),
1170
- onOpenSettings && /* @__PURE__ */ jsx("button", { onClick: onOpenSettings, style: styles.settingsButton, type: "button", children: "Settings" })
1171
- ] })
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
1172
1268
  ] });
1173
1269
  }
1174
1270
  var styles = {
1175
1271
  container: {
1176
- padding: "12px 16px",
1272
+ padding: "14px 16px",
1177
1273
  borderBottom: "1px solid #e5e7eb",
1178
1274
  backgroundColor: "#f9fafb"
1179
1275
  },
1180
- infoRow: {
1276
+ nameRow: {
1181
1277
  display: "flex",
1182
- justifyContent: "space-between",
1183
- alignItems: "center"
1278
+ alignItems: "center",
1279
+ gap: "6px"
1184
1280
  },
1185
- patientInfo: {
1281
+ backButton: {
1282
+ width: "36px",
1283
+ height: "36px",
1186
1284
  display: "flex",
1187
1285
  alignItems: "center",
1188
- gap: "8px",
1189
- flexWrap: "wrap"
1286
+ justifyContent: "center",
1287
+ marginLeft: "-8px",
1288
+ backgroundColor: "transparent",
1289
+ border: "none",
1290
+ borderRadius: "50%",
1291
+ cursor: "pointer",
1292
+ flexShrink: 0
1190
1293
  },
1191
- patientName: {
1294
+ nameText: {
1295
+ fontSize: "18px",
1192
1296
  fontWeight: 600,
1193
- fontSize: "17px",
1194
- color: "#111827"
1297
+ color: "#111827",
1298
+ lineHeight: 1.3,
1299
+ minWidth: 0,
1300
+ overflow: "hidden",
1301
+ textOverflow: "ellipsis",
1302
+ whiteSpace: "nowrap"
1195
1303
  },
1196
- secondaryRow: {
1304
+ metaRow: {
1197
1305
  display: "flex",
1198
- gap: "12px",
1199
- marginTop: "4px",
1200
- alignItems: "center"
1306
+ flexWrap: "wrap",
1307
+ columnGap: "14px",
1308
+ rowGap: "4px",
1309
+ marginTop: "6px"
1201
1310
  },
1202
- secondary: {
1311
+ metaItem: {
1203
1312
  fontSize: "13px",
1204
- color: "#4b5563"
1313
+ color: "#374151",
1314
+ whiteSpace: "nowrap"
1315
+ },
1316
+ metaLabel: {
1317
+ color: "#6b7280",
1318
+ fontWeight: 500
1319
+ },
1320
+ metaValue: {
1321
+ color: "#111827"
1205
1322
  },
1206
1323
  actions: {
1207
1324
  display: "flex",
1208
1325
  gap: "8px",
1209
- marginTop: "8px"
1326
+ marginTop: "10px"
1210
1327
  },
1211
1328
  dicomButton: {
1329
+ display: "inline-flex",
1330
+ alignItems: "center",
1331
+ gap: "8px",
1212
1332
  minHeight: "40px",
1213
1333
  padding: "8px 16px",
1214
1334
  fontSize: "14px",
@@ -1220,6 +1340,9 @@ var styles = {
1220
1340
  cursor: "pointer"
1221
1341
  },
1222
1342
  settingsButton: {
1343
+ display: "inline-flex",
1344
+ alignItems: "center",
1345
+ gap: "8px",
1223
1346
  minHeight: "40px",
1224
1347
  padding: "8px 16px",
1225
1348
  fontSize: "14px",
@@ -1436,6 +1559,8 @@ function MoreIcon({ size = 18, color = "currentColor" }) {
1436
1559
  }
1437
1560
  );
1438
1561
  }
1562
+ var VIEWPORT_MARGIN = 8;
1563
+ var TRIGGER_GAP = 4;
1439
1564
  function MessageActionsMenu({
1440
1565
  message,
1441
1566
  onReply,
@@ -1447,16 +1572,43 @@ function MessageActionsMenu({
1447
1572
  className
1448
1573
  }) {
1449
1574
  const [isOpen, setIsOpen] = useState(false);
1450
- const wrapperRef = useRef(null);
1575
+ const [coords, setCoords] = useState(null);
1451
1576
  const triggerRef = useRef(null);
1577
+ const menuRef = useRef(null);
1452
1578
  const isPinned = !!message.isPinned;
1453
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]);
1454
1607
  useEffect(() => {
1455
1608
  if (!isOpen) return;
1609
+ const isInside = (node) => !!node && (triggerRef.current?.contains(node) === true || menuRef.current?.contains(node) === true);
1456
1610
  const handleClick = (e) => {
1457
- if (!wrapperRef.current?.contains(e.target)) {
1458
- setIsOpen(false);
1459
- }
1611
+ if (!isInside(e.target)) setIsOpen(false);
1460
1612
  };
1461
1613
  const handleKey = (e) => {
1462
1614
  if (e.key === "Escape") {
@@ -1464,18 +1616,100 @@ function MessageActionsMenu({
1464
1616
  triggerRef.current?.focus();
1465
1617
  }
1466
1618
  };
1619
+ const handleScroll = (e) => {
1620
+ if (!isInside(e.target)) setIsOpen(false);
1621
+ };
1622
+ const handleResize = () => setIsOpen(false);
1467
1623
  document.addEventListener("mousedown", handleClick);
1468
1624
  document.addEventListener("keydown", handleKey);
1625
+ window.addEventListener("resize", handleResize);
1626
+ window.addEventListener("scroll", handleScroll, true);
1469
1627
  return () => {
1470
1628
  document.removeEventListener("mousedown", handleClick);
1471
1629
  document.removeEventListener("keydown", handleKey);
1630
+ window.removeEventListener("resize", handleResize);
1631
+ window.removeEventListener("scroll", handleScroll, true);
1472
1632
  };
1473
1633
  }, [isOpen]);
1474
1634
  const run = (fn) => {
1475
1635
  setIsOpen(false);
1476
1636
  fn();
1477
1637
  };
1478
- 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: [
1479
1713
  /* @__PURE__ */ jsx(
1480
1714
  "button",
1481
1715
  {
@@ -1493,75 +1727,7 @@ function MessageActionsMenu({
1493
1727
  children: /* @__PURE__ */ jsx(MoreIcon, { size: 18, color: "#4b5563" })
1494
1728
  }
1495
1729
  ),
1496
- isOpen && /* @__PURE__ */ jsxs(
1497
- "div",
1498
- {
1499
- role: "menu",
1500
- "aria-label": "Message actions",
1501
- style: {
1502
- ...styles4.menu,
1503
- ...anchorRight ? styles4.menuRight : styles4.menuLeft
1504
- },
1505
- children: [
1506
- /* @__PURE__ */ jsxs(
1507
- "button",
1508
- {
1509
- type: "button",
1510
- role: "menuitem",
1511
- onClick: () => run(() => onReply(message)),
1512
- style: styles4.item,
1513
- children: [
1514
- /* @__PURE__ */ jsx(ReplyIcon, { size: 18, color: "#374151" }),
1515
- /* @__PURE__ */ jsx("span", { children: "Reply" })
1516
- ]
1517
- }
1518
- ),
1519
- isPinned ? /* @__PURE__ */ jsxs(
1520
- "button",
1521
- {
1522
- type: "button",
1523
- role: "menuitem",
1524
- onClick: () => run(() => onUnpin(message.id)),
1525
- style: styles4.item,
1526
- children: [
1527
- /* @__PURE__ */ jsx(PinIcon, { size: 16, color: "#374151" }),
1528
- /* @__PURE__ */ jsx("span", { children: "Unpin" })
1529
- ]
1530
- }
1531
- ) : /* @__PURE__ */ jsxs(
1532
- "button",
1533
- {
1534
- type: "button",
1535
- role: "menuitem",
1536
- onClick: () => run(() => onPin(message.id)),
1537
- disabled: pinDisabled,
1538
- style: {
1539
- ...styles4.item,
1540
- ...pinDisabled ? styles4.itemDisabled : {}
1541
- },
1542
- title: pinDisabled ? "Pin limit reached (max 3)" : void 0,
1543
- children: [
1544
- /* @__PURE__ */ jsx(PinIcon, { size: 16, color: "#374151" }),
1545
- /* @__PURE__ */ jsx("span", { children: pinDisabled ? "Pin (limit reached)" : "Pin" })
1546
- ]
1547
- }
1548
- ),
1549
- canCopy && onCopy && /* @__PURE__ */ jsxs(
1550
- "button",
1551
- {
1552
- type: "button",
1553
- role: "menuitem",
1554
- onClick: () => run(() => onCopy(message.body)),
1555
- style: styles4.item,
1556
- children: [
1557
- /* @__PURE__ */ jsx(CopyIcon, { size: 16, color: "#374151" }),
1558
- /* @__PURE__ */ jsx("span", { children: "Copy text" })
1559
- ]
1560
- }
1561
- )
1562
- ]
1563
- }
1564
- )
1730
+ menu && typeof document !== "undefined" ? createPortal(menu, document.body) : null
1565
1731
  ] });
1566
1732
  }
1567
1733
  var styles4 = {
@@ -1582,22 +1748,15 @@ var styles4 = {
1582
1748
  color: "#4b5563"
1583
1749
  },
1584
1750
  menu: {
1585
- position: "absolute",
1586
- top: "100%",
1587
- marginTop: "4px",
1751
+ position: "fixed",
1588
1752
  minWidth: "160px",
1589
1753
  backgroundColor: "#ffffff",
1590
1754
  border: "1px solid #e5e7eb",
1591
1755
  borderRadius: "10px",
1592
1756
  boxShadow: "0 6px 18px rgba(0, 0, 0, 0.12)",
1593
1757
  padding: "4px",
1594
- zIndex: 20
1595
- },
1596
- menuLeft: {
1597
- left: 0
1598
- },
1599
- menuRight: {
1600
- right: 0
1758
+ // Above the floating popup (z-index 9999) but below any future modal.
1759
+ zIndex: 1e4
1601
1760
  },
1602
1761
  item: {
1603
1762
  display: "flex",
@@ -2250,31 +2409,34 @@ var MessageList = forwardRef(function MessageList2({
2250
2409
  /* @__PURE__ */ jsx("p", { style: styles6.emptySubtitle, children: "Start the conversation \u2014 everyone on this case will see what you write." })
2251
2410
  ] }),
2252
2411
  messages.length === 0 && isLoading && /* @__PURE__ */ jsx("div", { style: styles6.firstLoad, children: /* @__PURE__ */ jsx(Spinner, { size: 20 }) }),
2253
- messages.map((message, index) => {
2254
- const currentBucket = dayBucketKey(message.insertedAt);
2255
- const prevBucket = index === 0 ? null : dayBucketKey(messages[index - 1].insertedAt);
2256
- const showDivider = currentBucket && currentBucket !== prevBucket;
2257
- return /* @__PURE__ */ jsxs(React4.Fragment, { children: [
2258
- showDivider && /* @__PURE__ */ jsx("div", { style: styles6.dayDivider, role: "separator", "aria-label": "Date", children: /* @__PURE__ */ jsx("span", { style: styles6.dayDividerPill, children: formatDayDivider(message.insertedAt) }) }),
2259
- /* @__PURE__ */ jsx("div", { "data-message-id": message.id, children: /* @__PURE__ */ jsx(
2260
- MessageBubble,
2261
- {
2262
- message,
2263
- isOwn: message.senderId === currentUserId,
2264
- participants,
2265
- currentUserId,
2266
- showSeenBy,
2267
- onDeepLinkClick,
2268
- onReply,
2269
- onPin,
2270
- onUnpin,
2271
- onCopy,
2272
- onRetry,
2273
- pinDisabled
2274
- }
2275
- ) })
2276
- ] }, message.id);
2277
- }),
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
+ })(),
2278
2440
  typingUsers.length > 0 && /* @__PURE__ */ jsxs("div", { style: styles6.typingIndicator, children: [
2279
2441
  typingUsers.map((t) => t.userName ?? "Someone").join(", "),
2280
2442
  typingUsers.length === 1 ? " is " : " are ",
@@ -2354,7 +2516,11 @@ var styles6 = {
2354
2516
  dayDivider: {
2355
2517
  display: "flex",
2356
2518
  justifyContent: "center",
2357
- margin: "12px 0 8px"
2519
+ margin: "12px 0 8px",
2520
+ position: "sticky",
2521
+ top: "4px",
2522
+ zIndex: 1,
2523
+ pointerEvents: "none"
2358
2524
  },
2359
2525
  dayDividerPill: {
2360
2526
  display: "inline-block",
@@ -2364,7 +2530,11 @@ var styles6 = {
2364
2530
  color: "#334155",
2365
2531
  backgroundColor: "#e2e8f0",
2366
2532
  borderRadius: "12px",
2367
- 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"
2368
2538
  }
2369
2539
  };
2370
2540
  function ReplyPreview({ message, onCancel, className }) {
@@ -2515,6 +2685,7 @@ function MessageInput({
2515
2685
  }) {
2516
2686
  const [text, setText] = useState("");
2517
2687
  const [isSending, setIsSending] = useState(false);
2688
+ const [isUploading, setIsUploading] = useState(false);
2518
2689
  const [fileError, setFileError] = useState(null);
2519
2690
  const fileInputRef = useRef(null);
2520
2691
  const typingTimeoutRef = useRef(null);
@@ -2578,10 +2749,12 @@ function MessageInput({
2578
2749
  return;
2579
2750
  }
2580
2751
  setIsSending(true);
2752
+ setIsUploading(true);
2581
2753
  try {
2582
2754
  await onSendFile(file);
2583
2755
  } finally {
2584
2756
  setIsSending(false);
2757
+ setIsUploading(false);
2585
2758
  if (fileInputRef.current) fileInputRef.current.value = "";
2586
2759
  }
2587
2760
  },
@@ -2589,6 +2762,10 @@ function MessageInput({
2589
2762
  );
2590
2763
  return /* @__PURE__ */ jsxs("div", { className, style: styles8.container, children: [
2591
2764
  replyTo && onCancelReply && /* @__PURE__ */ jsx(ReplyPreview, { message: replyTo, onCancel: onCancelReply }),
2765
+ isUploading && /* @__PURE__ */ jsxs("div", { style: styles8.uploadBanner, children: [
2766
+ /* @__PURE__ */ jsx("span", { style: styles8.uploadSpinner }),
2767
+ "Uploading file\u2026"
2768
+ ] }),
2592
2769
  fileError && /* @__PURE__ */ jsxs("div", { style: styles8.error, role: "alert", children: [
2593
2770
  /* @__PURE__ */ jsx("span", { style: styles8.errorText, children: fileError }),
2594
2771
  /* @__PURE__ */ jsx(
@@ -2674,6 +2851,26 @@ var styles8 = {
2674
2851
  borderTop: "1px solid #e5e7eb",
2675
2852
  backgroundColor: "#ffffff"
2676
2853
  },
2854
+ uploadBanner: {
2855
+ display: "flex",
2856
+ alignItems: "center",
2857
+ gap: "8px",
2858
+ padding: "6px 14px",
2859
+ fontSize: "12px",
2860
+ color: "#6b7280",
2861
+ backgroundColor: "#f9fafb",
2862
+ borderBottom: "1px solid #e5e7eb"
2863
+ },
2864
+ uploadSpinner: {
2865
+ display: "inline-block",
2866
+ width: "12px",
2867
+ height: "12px",
2868
+ border: "2px solid #d1d5db",
2869
+ borderTopColor: "#7c3aed",
2870
+ borderRadius: "50%",
2871
+ animation: "natoe-colab-spin 0.7s linear infinite",
2872
+ flexShrink: 0
2873
+ },
2677
2874
  error: {
2678
2875
  display: "flex",
2679
2876
  alignItems: "center",
@@ -3416,11 +3613,52 @@ var styles12 = {
3416
3613
  letterSpacing: "0.4px"
3417
3614
  }
3418
3615
  };
3616
+
3617
+ // src/core/styles.ts
3618
+ var FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
3619
+ var ROOT_CLASS = "natoe-colab-root";
3620
+ var GLOBAL_STYLE_ID = "natoe-colab-global-styles";
3621
+ var GLOBAL_CSS = `
3622
+ @keyframes natoe-colab-spin { to { transform: rotate(360deg); } }
3623
+
3624
+ @keyframes natoe-colab-message-in {
3625
+ from { opacity: 0; transform: translateY(4px); }
3626
+ to { opacity: 1; transform: translateY(0); }
3627
+ }
3628
+
3629
+ /* High-contrast adjustments \u2014 older users on OS-level high-contrast mode
3630
+ get heavier borders and stronger text without losing the package look. */
3631
+ @media (prefers-contrast: more) {
3632
+ .${ROOT_CLASS} {
3633
+ color: #000000;
3634
+ }
3635
+ .${ROOT_CLASS} button {
3636
+ outline: 1px solid currentColor;
3637
+ }
3638
+ }
3639
+
3640
+ /* Honour reduced-motion preferences by killing entry animations. */
3641
+ @media (prefers-reduced-motion: reduce) {
3642
+ .${ROOT_CLASS} [data-natoe-message-bubble] {
3643
+ animation: none !important;
3644
+ }
3645
+ }
3646
+ `;
3647
+ function ensureGlobalStyles() {
3648
+ if (typeof document === "undefined") return;
3649
+ if (document.getElementById(GLOBAL_STYLE_ID)) return;
3650
+ const style = document.createElement("style");
3651
+ style.id = GLOBAL_STYLE_ID;
3652
+ style.textContent = GLOBAL_CSS;
3653
+ document.head.appendChild(style);
3654
+ }
3655
+ ensureGlobalStyles();
3419
3656
  function CollabPanel({
3420
3657
  orderId,
3421
3658
  patientData,
3422
3659
  participantIds,
3423
3660
  showSeenBy = true,
3661
+ onBack,
3424
3662
  className,
3425
3663
  style
3426
3664
  }) {
@@ -3494,13 +3732,13 @@ function CollabPanel({
3494
3732
  };
3495
3733
  const pinDisabled = pinnedMessages.length >= MAX_PINNED_MESSAGES;
3496
3734
  if (error) {
3497
- return /* @__PURE__ */ jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxs("div", { style: panelStyles.errorState, children: [
3735
+ return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsxs("div", { style: panelStyles.errorState, children: [
3498
3736
  /* @__PURE__ */ jsx("p", { style: panelStyles.errorTitle, children: "Unable to load conversation" }),
3499
3737
  /* @__PURE__ */ jsx("p", { style: panelStyles.errorMessage, children: error })
3500
3738
  ] }) });
3501
3739
  }
3502
3740
  if (isLoading && !conversation) {
3503
- return /* @__PURE__ */ jsx("div", { className, style: { ...panelStyles.container, ...style }, children: /* @__PURE__ */ jsx("div", { style: panelStyles.loadingState, children: "Loading conversation..." }) });
3741
+ 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..." }) });
3504
3742
  }
3505
3743
  return /* @__PURE__ */ jsxs("div", { className, style: { ...panelStyles.container, ...style }, children: [
3506
3744
  showSettings && conversation ? /* @__PURE__ */ jsx(
@@ -3528,7 +3766,8 @@ function CollabPanel({
3528
3766
  patientData,
3529
3767
  participants,
3530
3768
  onOpenDicom: handleOpenDicom,
3531
- onOpenSettings: () => setShowSettings(true)
3769
+ onOpenSettings: () => setShowSettings(true),
3770
+ onBack
3532
3771
  }
3533
3772
  ),
3534
3773
  pinnedMessages.length > 0 && /* @__PURE__ */ jsx(
@@ -3586,7 +3825,7 @@ var panelStyles = {
3586
3825
  borderRadius: "8px",
3587
3826
  overflow: "hidden",
3588
3827
  border: "1px solid #e5e7eb",
3589
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
3828
+ fontFamily: FONT_STACK
3590
3829
  },
3591
3830
  loadingState: {
3592
3831
  display: "flex",
@@ -3645,6 +3884,7 @@ function ExpandIcon({ size = 18, color = "currentColor" }) {
3645
3884
  }
3646
3885
  );
3647
3886
  }
3887
+ ensureGlobalStyles();
3648
3888
  var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3649
3889
  function CollabPopup({
3650
3890
  orderId,
@@ -3746,7 +3986,7 @@ function CollabPopup({
3746
3986
  "div",
3747
3987
  {
3748
3988
  ref: containerRef,
3749
- className,
3989
+ className: [ROOT_CLASS, className].filter(Boolean).join(" "),
3750
3990
  role: "dialog",
3751
3991
  "aria-modal": "true",
3752
3992
  "aria-labelledby": titleId,
@@ -3815,7 +4055,8 @@ var styles13 = {
3815
4055
  flexDirection: "column",
3816
4056
  backgroundColor: "#ffffff",
3817
4057
  border: "1px solid #e5e7eb",
3818
- transition: "height 0.2s ease"
4058
+ transition: "height 0.2s ease",
4059
+ fontFamily: FONT_STACK
3819
4060
  },
3820
4061
  titleBar: {
3821
4062
  display: "flex",
@@ -4087,6 +4328,7 @@ function useInlineCollab({
4087
4328
  containerRef
4088
4329
  };
4089
4330
  }
4331
+ ensureGlobalStyles();
4090
4332
  function CollabInline({
4091
4333
  orderId,
4092
4334
  patientData,
@@ -4109,9 +4351,9 @@ function CollabInline({
4109
4351
  containerRef
4110
4352
  } = useInlineCollab({ orderId, patientData, participantIds, messageLimit });
4111
4353
  if (isLoading && !hasConversation) {
4112
- return /* @__PURE__ */ jsx("div", { className, style: { ...styles14.container, ...style }, children: /* @__PURE__ */ jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4354
+ return /* @__PURE__ */ jsx("div", { className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: /* @__PURE__ */ jsx("div", { style: styles14.loadingState, children: "Loading..." }) });
4113
4355
  }
4114
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className, style: { ...styles14.container, ...style }, children: [
4356
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: [ROOT_CLASS, className].filter(Boolean).join(" "), style: { ...styles14.container, ...style }, children: [
4115
4357
  hasConversation && messages.length > 0 && /* @__PURE__ */ jsx("div", { style: styles14.preview, children: messages.slice(-messageLimit).map((message) => /* @__PURE__ */ jsx(InlineMessageRow, { message }, message.id)) }),
4116
4358
  error && /* @__PURE__ */ jsx("div", { style: styles14.error, children: error }),
4117
4359
  /* @__PURE__ */ jsx(
@@ -4327,7 +4569,7 @@ var styles14 = {
4327
4569
  backgroundColor: "#ffffff",
4328
4570
  border: "1px solid #e5e7eb",
4329
4571
  borderRadius: "8px",
4330
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
4572
+ fontFamily: FONT_STACK
4331
4573
  },
4332
4574
  loadingState: {
4333
4575
  padding: "8px",
@@ -4957,6 +5199,8 @@ var styles16 = {
4957
5199
  textAlign: "center"
4958
5200
  }
4959
5201
  };
5202
+ ensureGlobalStyles();
5203
+ var COMPACT_BREAKPOINT = 720;
4960
5204
  function CollabInbox({
4961
5205
  initialConversationId,
4962
5206
  onSelectConversation,
@@ -4966,37 +5210,70 @@ function CollabInbox({
4966
5210
  }) {
4967
5211
  const { conversations, isLoading, error } = useConversationList();
4968
5212
  const [selectedId, setSelectedId] = useState(initialConversationId ?? null);
5213
+ const [isCompact, setIsCompact] = useState(
5214
+ () => typeof window === "undefined" ? false : window.innerWidth < COMPACT_BREAKPOINT
5215
+ );
5216
+ useEffect(() => {
5217
+ if (typeof window === "undefined") return;
5218
+ const mq = window.matchMedia(`(max-width: ${COMPACT_BREAKPOINT - 1}px)`);
5219
+ const sync = () => setIsCompact(mq.matches);
5220
+ sync();
5221
+ mq.addEventListener("change", sync);
5222
+ return () => mq.removeEventListener("change", sync);
5223
+ }, []);
4969
5224
  const selected = conversations.find((c) => c.id === selectedId) || null;
4970
5225
  const handleSelect = (item) => {
4971
5226
  setSelectedId(item.id);
4972
5227
  onSelectConversation?.(item);
4973
5228
  };
4974
- return /* @__PURE__ */ jsxs("div", { className, style: { ...styles17.container, ...style }, children: [
4975
- /* @__PURE__ */ jsx("div", { style: styles17.sidebar, children: /* @__PURE__ */ jsx(
4976
- ConversationList,
4977
- {
4978
- conversations,
4979
- selectedId,
4980
- isLoading,
4981
- error,
4982
- onSelect: handleSelect,
4983
- title
4984
- }
4985
- ) }),
4986
- /* @__PURE__ */ jsx("div", { style: styles17.main, children: selected ? /* @__PURE__ */ jsx(
4987
- CollabPanel,
4988
- {
4989
- orderId: selected.orderId,
4990
- patientData: buildPatientData(selected),
4991
- showSeenBy: true,
4992
- style: styles17.panel
4993
- },
4994
- selected.id
4995
- ) : /* @__PURE__ */ jsx(EmptyState, { hasAny: conversations.length > 0 }) })
4996
- ] });
5229
+ const showDetail = !isCompact || isCompact && !!selected;
5230
+ const showList = !isCompact || isCompact && !selected;
5231
+ return /* @__PURE__ */ jsxs(
5232
+ "div",
5233
+ {
5234
+ className: [ROOT_CLASS, className].filter(Boolean).join(" "),
5235
+ style: { ...styles17.container, ...style },
5236
+ children: [
5237
+ showList && /* @__PURE__ */ jsx("div", { style: isCompact ? styles17.sidebarCompact : styles17.sidebar, children: /* @__PURE__ */ jsx(
5238
+ ConversationList,
5239
+ {
5240
+ conversations,
5241
+ selectedId,
5242
+ isLoading,
5243
+ error,
5244
+ onSelect: handleSelect,
5245
+ title
5246
+ }
5247
+ ) }),
5248
+ showDetail && /* @__PURE__ */ jsx("div", { style: isCompact ? styles17.mainCompact : styles17.main, children: selected ? /* @__PURE__ */ jsx(
5249
+ CollabPanel,
5250
+ {
5251
+ orderId: selected.orderId,
5252
+ patientData: buildPatientData(selected),
5253
+ showSeenBy: true,
5254
+ style: styles17.panel,
5255
+ onBack: isCompact ? () => setSelectedId(null) : void 0
5256
+ },
5257
+ selected.id
5258
+ ) : /* @__PURE__ */ jsx(EmptyState, { hasAny: conversations.length > 0 }) })
5259
+ ]
5260
+ }
5261
+ );
4997
5262
  }
4998
5263
  function buildPatientData(item) {
4999
5264
  if (item.patientSnapshot) return item.patientSnapshot;
5265
+ const parts = item.name.split(" - ");
5266
+ if (parts.length >= 3) {
5267
+ const last4 = parts[parts.length - 1];
5268
+ const labName = parts[parts.length - 2];
5269
+ const patientName = parts.slice(0, parts.length - 2).join(" - ");
5270
+ return {
5271
+ patientName,
5272
+ labName: labName || void 0,
5273
+ orderId: item.orderId,
5274
+ displayOrderId: last4
5275
+ };
5276
+ }
5000
5277
  return {
5001
5278
  patientName: item.name,
5002
5279
  orderId: item.orderId
@@ -5015,7 +5292,7 @@ var styles17 = {
5015
5292
  height: "100%",
5016
5293
  width: "100%",
5017
5294
  backgroundColor: "#ffffff",
5018
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
5295
+ fontFamily: FONT_STACK
5019
5296
  },
5020
5297
  sidebar: {
5021
5298
  width: "320px",
@@ -5024,6 +5301,12 @@ var styles17 = {
5024
5301
  display: "flex",
5025
5302
  flexDirection: "column"
5026
5303
  },
5304
+ sidebarCompact: {
5305
+ width: "100%",
5306
+ flex: 1,
5307
+ display: "flex",
5308
+ flexDirection: "column"
5309
+ },
5027
5310
  main: {
5028
5311
  flex: 1,
5029
5312
  display: "flex",
@@ -5032,6 +5315,13 @@ var styles17 = {
5032
5315
  padding: "12px",
5033
5316
  backgroundColor: "#f3f4f6"
5034
5317
  },
5318
+ mainCompact: {
5319
+ flex: 1,
5320
+ display: "flex",
5321
+ flexDirection: "column",
5322
+ minWidth: 0,
5323
+ backgroundColor: "#f3f4f6"
5324
+ },
5035
5325
  panel: {
5036
5326
  height: "100%",
5037
5327
  width: "100%"