@natoe/colab 0.1.16 → 0.1.18

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
@@ -513,8 +513,6 @@ var SHADOW = {
513
513
  toast: "0 4px 12px rgba(0, 0, 0, 0.18)"
514
514
  };
515
515
  var Z_INDEX = {
516
- /** Sticky day divider — above bubbles, below interactive popovers. */
517
- sticky: 1,
518
516
  /** Toasts inside a panel. */
519
517
  toast: 20,
520
518
  /** Floating CollabPopup dialog. */
@@ -1446,6 +1444,31 @@ function DicomIcon({ size = 18, color = "currentColor" }) {
1446
1444
  }
1447
1445
  );
1448
1446
  }
1447
+ function OpenCaseIcon({
1448
+ size = 18,
1449
+ color = "currentColor"
1450
+ }) {
1451
+ return /* @__PURE__ */ jsxs(
1452
+ "svg",
1453
+ {
1454
+ xmlns: "http://www.w3.org/2000/svg",
1455
+ width: size,
1456
+ height: size,
1457
+ viewBox: "0 0 24 24",
1458
+ fill: "none",
1459
+ stroke: color,
1460
+ strokeWidth: "2",
1461
+ strokeLinecap: "round",
1462
+ strokeLinejoin: "round",
1463
+ "aria-hidden": "true",
1464
+ children: [
1465
+ /* @__PURE__ */ jsx("path", { d: "M14 4h6v6" }),
1466
+ /* @__PURE__ */ jsx("path", { d: "M20 4L11 13" }),
1467
+ /* @__PURE__ */ jsx("path", { d: "M20 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5" })
1468
+ ]
1469
+ }
1470
+ );
1471
+ }
1449
1472
  function PeopleIcon({ size = 18, color = "currentColor" }) {
1450
1473
  return /* @__PURE__ */ jsxs(
1451
1474
  "svg",
@@ -1483,15 +1506,19 @@ function BackIcon({ size = 22, color = "currentColor" }) {
1483
1506
  }
1484
1507
  );
1485
1508
  }
1509
+ function stripOrderIdSuffix(name) {
1510
+ return name.replace(/\s*-\s*\d{1,6}\s*$/, "").trim();
1511
+ }
1486
1512
  function resolveDisplayName(patientData, displayName) {
1487
1513
  const localComplete = !!patientData.labName && !!patientData.displayOrderId;
1488
- if (localComplete) return buildChannelName(patientData);
1489
- return cleanChannelName(displayName) || buildChannelName(patientData);
1514
+ const raw = localComplete ? buildChannelName(patientData) : cleanChannelName(displayName) || buildChannelName(patientData);
1515
+ return stripOrderIdSuffix(raw);
1490
1516
  }
1491
1517
  function PatientHeader({
1492
1518
  patientData,
1493
1519
  participants,
1494
1520
  onOpenDicom,
1521
+ onOpenCase,
1495
1522
  onOpenSettings,
1496
1523
  onBack,
1497
1524
  hideName = false,
@@ -1499,29 +1526,58 @@ function PatientHeader({
1499
1526
  className
1500
1527
  }) {
1501
1528
  const hasDicom = !!(patientData.studyId && patientData.storageId);
1502
- const meta = [];
1503
- if (patientData.patientAge && patientData.patientSex) {
1504
- meta.push({
1505
- key: "ageSex",
1506
- label: "Age / Sex",
1507
- value: `${patientData.patientAge} \xB7 ${patientData.patientSex}`
1508
- });
1509
- } else if (patientData.patientAge) {
1510
- meta.push({ key: "age", label: "Age", value: String(patientData.patientAge) });
1511
- } else if (patientData.patientSex) {
1512
- meta.push({ key: "sex", label: "Sex", value: String(patientData.patientSex) });
1513
- }
1514
- if (patientData.studyType) {
1515
- meta.push({ key: "modality", label: "Modality", value: patientData.studyType });
1516
- }
1529
+ const resolvedName = resolveDisplayName(patientData, displayName);
1530
+ const metaParts = [];
1531
+ if (patientData.patientAge) metaParts.push(String(patientData.patientAge));
1532
+ if (patientData.patientSex) metaParts.push(String(patientData.patientSex));
1533
+ if (patientData.studyType) metaParts.push(patientData.studyType);
1517
1534
  if (patientData.bodyParts && patientData.bodyParts.length > 0) {
1518
- meta.push({
1519
- key: "body",
1520
- label: "Body part",
1521
- value: patientData.bodyParts.join(", ")
1522
- });
1535
+ metaParts.push(patientData.bodyParts.join(", "));
1523
1536
  }
1524
- const hasActions = hasDicom && onOpenDicom || onOpenSettings;
1537
+ const hasPrimaryActions = hasDicom && onOpenDicom || onOpenCase;
1538
+ const primaryActions = hasPrimaryActions ? /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
1539
+ hasDicom && onOpenDicom && /* @__PURE__ */ jsxs(
1540
+ "button",
1541
+ {
1542
+ onClick: onOpenDicom,
1543
+ style: styles.dicomButton,
1544
+ type: "button",
1545
+ "aria-label": "View DICOM study",
1546
+ children: [
1547
+ /* @__PURE__ */ jsx(DicomIcon, { size: 18, color: COLOR.primary }),
1548
+ /* @__PURE__ */ jsx("span", { children: "View DICOM" })
1549
+ ]
1550
+ }
1551
+ ),
1552
+ onOpenCase && /* @__PURE__ */ jsxs(
1553
+ "button",
1554
+ {
1555
+ onClick: onOpenCase,
1556
+ style: styles.openCaseButton,
1557
+ type: "button",
1558
+ "aria-label": "Open case",
1559
+ title: "Open case",
1560
+ children: [
1561
+ /* @__PURE__ */ jsx(OpenCaseIcon, { size: 18, color: COLOR.primary }),
1562
+ /* @__PURE__ */ jsx("span", { children: "Open" })
1563
+ ]
1564
+ }
1565
+ )
1566
+ ] }) : null;
1567
+ const settingsButton = onOpenSettings ? /* @__PURE__ */ jsxs(
1568
+ "button",
1569
+ {
1570
+ onClick: onOpenSettings,
1571
+ style: styles.settingsIconButton,
1572
+ type: "button",
1573
+ "aria-label": `Open channel settings (${participants.length} participants)`,
1574
+ title: "Channel participants",
1575
+ children: [
1576
+ /* @__PURE__ */ jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
1577
+ /* @__PURE__ */ jsx("span", { style: styles.participantCount, children: participants.length })
1578
+ ]
1579
+ }
1580
+ ) : null;
1525
1581
  return /* @__PURE__ */ jsxs("div", { className, style: styles.container, children: [
1526
1582
  !hideName && /* @__PURE__ */ jsxs("div", { style: styles.nameRow, children: [
1527
1583
  onBack && /* @__PURE__ */ jsx(
@@ -1532,45 +1588,19 @@ function PatientHeader({
1532
1588
  style: styles.backButton,
1533
1589
  "aria-label": "Back to conversations",
1534
1590
  title: "Back to conversations",
1535
- children: /* @__PURE__ */ jsx(BackIcon, { size: 22, color: COLOR.neutral800 })
1591
+ children: /* @__PURE__ */ jsx(BackIcon, { size: 20, color: COLOR.neutral800 })
1536
1592
  }
1537
1593
  ),
1538
- /* @__PURE__ */ jsx("span", { style: styles.nameText, children: resolveDisplayName(patientData, displayName) })
1594
+ /* @__PURE__ */ jsx("span", { style: styles.nameText, children: resolvedName }),
1595
+ primaryActions
1539
1596
  ] }),
1540
- /* @__PURE__ */ jsxs("div", { style: styles.caseCard, children: [
1541
- meta.length > 0 && /* @__PURE__ */ jsx("div", { style: styles.metaRow, children: meta.map((item) => /* @__PURE__ */ jsxs("div", { style: styles.metaCol, children: [
1542
- /* @__PURE__ */ jsx("span", { style: styles.metaLabel, children: item.label }),
1543
- /* @__PURE__ */ jsx("span", { style: styles.metaValue, children: item.value })
1544
- ] }, item.key)) }),
1545
- hasActions && /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
1546
- hasDicom && onOpenDicom && /* @__PURE__ */ jsxs(
1547
- "button",
1548
- {
1549
- onClick: onOpenDicom,
1550
- style: styles.dicomButton,
1551
- type: "button",
1552
- "aria-label": "View DICOM study",
1553
- children: [
1554
- /* @__PURE__ */ jsx(DicomIcon, { size: 18, color: COLOR.primary }),
1555
- /* @__PURE__ */ jsx("span", { children: "View DICOM" })
1556
- ]
1557
- }
1558
- ),
1559
- onOpenSettings && /* @__PURE__ */ jsxs(
1560
- "button",
1561
- {
1562
- onClick: onOpenSettings,
1563
- style: styles.settingsIconButton,
1564
- type: "button",
1565
- "aria-label": `Open channel settings (${participants.length} participants)`,
1566
- title: "Channel participants",
1567
- children: [
1568
- /* @__PURE__ */ jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
1569
- /* @__PURE__ */ jsx("span", { style: styles.participantCount, children: participants.length })
1570
- ]
1571
- }
1572
- )
1573
- ] })
1597
+ !hideName && (metaParts.length > 0 || settingsButton) && /* @__PURE__ */ jsxs("div", { style: styles.metaRow, children: [
1598
+ metaParts.length > 0 && /* @__PURE__ */ jsx("span", { style: styles.metaText, children: metaParts.join(" \xB7 ") }),
1599
+ settingsButton && /* @__PURE__ */ jsx("div", { style: styles.metaRowEnd, children: settingsButton })
1600
+ ] }),
1601
+ hideName && (primaryActions || settingsButton) && /* @__PURE__ */ jsxs("div", { style: styles.actionsOnlyRow, children: [
1602
+ primaryActions,
1603
+ settingsButton
1574
1604
  ] })
1575
1605
  ] });
1576
1606
  }
@@ -1579,16 +1609,16 @@ var styles = {
1579
1609
  display: "flex",
1580
1610
  flexDirection: "column",
1581
1611
  backgroundColor: COLOR.primaryBg,
1582
- borderBottom: `1px solid ${COLOR.neutral200}`
1612
+ borderBottom: `1px solid ${COLOR.neutral200}`,
1613
+ padding: `${SPACE.S2} ${SPACE.S4}`,
1614
+ gap: "2px"
1583
1615
  },
1584
- // Optional name row, only when `hideName` is false (compact-inbox path)
1616
+ // Title row back · name · actions in one horizontal line.
1585
1617
  nameRow: {
1586
1618
  display: "flex",
1587
1619
  alignItems: "center",
1588
1620
  gap: SPACE.S2,
1589
- padding: `${SPACE.S3} ${SPACE.S4} ${SPACE.S2}`,
1590
- borderBottom: `1px solid ${COLOR.neutral200}`,
1591
- backgroundColor: COLOR.white
1621
+ minWidth: 0
1592
1622
  },
1593
1623
  backButton: {
1594
1624
  width: SIZE.controlIcon,
@@ -1604,57 +1634,51 @@ var styles = {
1604
1634
  flexShrink: 0
1605
1635
  },
1606
1636
  nameText: {
1637
+ flex: 1,
1638
+ minWidth: 0,
1607
1639
  fontSize: FONT_SIZE.lg,
1608
1640
  fontWeight: FONT_WEIGHT.bold,
1609
1641
  color: COLOR.neutral900,
1610
1642
  lineHeight: LINE_HEIGHT.tight,
1611
1643
  letterSpacing: "-0.01em",
1612
- minWidth: 0,
1613
1644
  overflow: "hidden",
1614
1645
  textOverflow: "ellipsis",
1615
1646
  whiteSpace: "nowrap"
1616
1647
  },
1617
- // Case card: meta columns flowing in a wrap-row, actions stacked
1618
- // below. Each meta column is a small label-above-value pair so the
1619
- // user can scan field names quickly without a legend.
1620
- caseCard: {
1621
- display: "flex",
1622
- flexDirection: "column",
1623
- gap: SPACE.S3,
1624
- padding: `${SPACE.S3} ${SPACE.S5}`
1625
- },
1626
- // Wrap-row of stacked label/value columns. Compact column gap keeps
1627
- // the row dense without crowding; row gap kicks in when columns
1628
- // wrap to a second line on narrow popups.
1648
+ // Sub-row beneath the title small muted meta on the left, the
1649
+ // channel-settings pill on the right when present. Meta text can
1650
+ // ellipsis on narrow widths; the settings pill stays its content
1651
+ // width and hugs the trailing edge via `metaRowEnd`.
1629
1652
  metaRow: {
1630
1653
  display: "flex",
1631
- flexWrap: "wrap",
1632
- columnGap: SPACE.S5,
1633
- rowGap: SPACE.S2,
1654
+ alignItems: "center",
1655
+ gap: SPACE.S2,
1656
+ paddingLeft: `calc(${SIZE.controlIcon} + ${SPACE.S2} - ${SPACE.S2})`,
1634
1657
  minWidth: 0
1635
1658
  },
1636
- metaCol: {
1659
+ metaRowEnd: {
1660
+ marginLeft: "auto",
1661
+ flexShrink: 0
1662
+ },
1663
+ // When hideName=true (popup mode), only the actions render. The
1664
+ // popup's own title bar carries the name + meta above this row, so
1665
+ // the actions land flush-left here — pinning them to the right
1666
+ // edge would leave the whole left half of the strip awkwardly
1667
+ // empty (the meta that used to fill it now lives in the title bar).
1668
+ actionsOnlyRow: {
1637
1669
  display: "flex",
1638
- flexDirection: "column",
1639
- gap: "2px",
1670
+ justifyContent: "flex-start",
1640
1671
  minWidth: 0
1641
1672
  },
1642
- metaLabel: {
1643
- fontSize: "11px",
1644
- fontWeight: FONT_WEIGHT.bold,
1645
- letterSpacing: "0.06em",
1646
- textTransform: "uppercase",
1647
- color: COLOR.neutral500,
1648
- whiteSpace: "nowrap"
1649
- },
1650
- metaValue: {
1673
+ metaText: {
1651
1674
  fontSize: FONT_SIZE.sm,
1652
- fontWeight: FONT_WEIGHT.semibold,
1653
- color: COLOR.neutral900,
1654
- whiteSpace: "nowrap",
1675
+ fontWeight: FONT_WEIGHT.medium,
1676
+ color: COLOR.neutral600,
1677
+ lineHeight: LINE_HEIGHT.tight,
1655
1678
  overflow: "hidden",
1656
1679
  textOverflow: "ellipsis",
1657
- maxWidth: "180px"
1680
+ whiteSpace: "nowrap",
1681
+ minWidth: 0
1658
1682
  },
1659
1683
  actions: {
1660
1684
  display: "flex",
@@ -1666,7 +1690,24 @@ var styles = {
1666
1690
  alignItems: "center",
1667
1691
  gap: SPACE.S2,
1668
1692
  minHeight: SIZE.control,
1669
- padding: `${SPACE.S2} 14px`,
1693
+ padding: `${SPACE.S1} ${SPACE.S3}`,
1694
+ fontSize: FONT_SIZE.sm,
1695
+ fontWeight: FONT_WEIGHT.semibold,
1696
+ color: COLOR.primary,
1697
+ backgroundColor: COLOR.white,
1698
+ border: `1.5px solid ${COLOR.primary}`,
1699
+ borderRadius: RADIUS.lg,
1700
+ cursor: "pointer"
1701
+ },
1702
+ // Sibling of the DICOM button — same shape so the two read as a pair.
1703
+ // Click hands off to the host's `onOpenCase` callback (role-aware
1704
+ // routing lives on the host side).
1705
+ openCaseButton: {
1706
+ display: "inline-flex",
1707
+ alignItems: "center",
1708
+ gap: SPACE.S2,
1709
+ minHeight: SIZE.control,
1710
+ padding: `${SPACE.S1} ${SPACE.S3}`,
1670
1711
  fontSize: FONT_SIZE.sm,
1671
1712
  fontWeight: FONT_WEIGHT.semibold,
1672
1713
  color: COLOR.primary,
@@ -1675,10 +1716,8 @@ var styles = {
1675
1716
  borderRadius: RADIUS.lg,
1676
1717
  cursor: "pointer"
1677
1718
  },
1678
- // Channel-info button: people icon + participant count. Click opens the
1679
- // channel settings overlay (kept on the same handler as before so hosts
1680
- // don't have to re-wire — the affordance just looks like a "members"
1681
- // pill now instead of a gear).
1719
+ // Channel-info button: people icon + participant count. Opens the
1720
+ // channel settings overlay.
1682
1721
  settingsIconButton: {
1683
1722
  minHeight: SIZE.control,
1684
1723
  display: "inline-flex",
@@ -1821,7 +1860,6 @@ var styles3 = {
1821
1860
  display: "flex",
1822
1861
  alignItems: "center",
1823
1862
  gap: SPACE.S1,
1824
- marginTop: "2px",
1825
1863
  fontSize: FONT_SIZE.xs,
1826
1864
  opacity: 0.85
1827
1865
  },
@@ -2158,6 +2196,62 @@ function AlertIcon({ size = 14, color = "currentColor" }) {
2158
2196
  }
2159
2197
  );
2160
2198
  }
2199
+ function RoleAvatarIcon({
2200
+ role,
2201
+ size = 18,
2202
+ color = "currentColor",
2203
+ className
2204
+ }) {
2205
+ const common = {
2206
+ width: size,
2207
+ height: size,
2208
+ viewBox: "0 0 24 24",
2209
+ fill: "none",
2210
+ stroke: color,
2211
+ strokeWidth: 1.9,
2212
+ strokeLinecap: "round",
2213
+ strokeLinejoin: "round",
2214
+ "aria-hidden": true,
2215
+ className
2216
+ };
2217
+ switch (role) {
2218
+ case "radiologist":
2219
+ return /* @__PURE__ */ jsxs("svg", { ...common, children: [
2220
+ /* @__PURE__ */ jsx("path", { d: "M7.5 8.5h9" }),
2221
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "9.5", r: "4" }),
2222
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "7.5", r: "1", fill: color, stroke: "none" }),
2223
+ /* @__PURE__ */ jsx("path", { d: "M4 21v-1.5a5 5 0 0 1 5-5h6a5 5 0 0 1 5 5V21" })
2224
+ ] });
2225
+ case "lab":
2226
+ return /* @__PURE__ */ jsxs("svg", { ...common, children: [
2227
+ /* @__PURE__ */ jsx("path", { d: "M3 21V9l9-6 9 6v12" }),
2228
+ /* @__PURE__ */ jsx("path", { d: "M12 11v6" }),
2229
+ /* @__PURE__ */ jsx("path", { d: "M9 14h6" }),
2230
+ /* @__PURE__ */ jsx("path", { d: "M11 21v-3h2v3" }),
2231
+ /* @__PURE__ */ jsx("path", { d: "M3 21h18" })
2232
+ ] });
2233
+ case "physician":
2234
+ return /* @__PURE__ */ jsxs("svg", { ...common, children: [
2235
+ /* @__PURE__ */ jsx("path", { d: "M7 3v5a4 4 0 0 0 8 0V3" }),
2236
+ /* @__PURE__ */ jsx("path", { d: "M5 3h2" }),
2237
+ /* @__PURE__ */ jsx("path", { d: "M15 3h2" }),
2238
+ /* @__PURE__ */ jsx("path", { d: "M11 12v3a5 5 0 0 0 5 5h0a5 5 0 0 0 5-5v-2" }),
2239
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "11", r: "2" })
2240
+ ] });
2241
+ case "admin":
2242
+ return /* @__PURE__ */ jsxs("svg", { ...common, children: [
2243
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
2244
+ /* @__PURE__ */ jsx("path", { d: "M12 2v3" }),
2245
+ /* @__PURE__ */ jsx("path", { d: "M12 19v3" }),
2246
+ /* @__PURE__ */ jsx("path", { d: "M5 12H2" }),
2247
+ /* @__PURE__ */ jsx("path", { d: "M22 12h-3" }),
2248
+ /* @__PURE__ */ jsx("path", { d: "M5.6 5.6l2 2" }),
2249
+ /* @__PURE__ */ jsx("path", { d: "M16.4 16.4l2 2" }),
2250
+ /* @__PURE__ */ jsx("path", { d: "M5.6 18.4l2-2" }),
2251
+ /* @__PURE__ */ jsx("path", { d: "M16.4 7.6l2-2" })
2252
+ ] });
2253
+ }
2254
+ }
2161
2255
  function Spinner({ size = 16, color = COLOR.neutral500, thickness = 2 }) {
2162
2256
  return /* @__PURE__ */ jsx(
2163
2257
  "span",
@@ -2246,12 +2340,11 @@ function MessageBubble({
2246
2340
  ...styles5.authorAvatar,
2247
2341
  backgroundColor: roleColor(message.senderRole)
2248
2342
  },
2249
- "aria-hidden": "true",
2250
- children: computeInitials(message.senderName)
2343
+ "aria-label": ROLE_LABELS[message.senderRole] ?? message.senderRole,
2344
+ children: /* @__PURE__ */ jsx(RoleAvatarIcon, { role: message.senderRole, size: 20, color: COLOR.white })
2251
2345
  }
2252
2346
  ),
2253
- /* @__PURE__ */ jsx("span", { style: styles5.authorName, children: message.senderName }),
2254
- /* @__PURE__ */ jsx(RoleBadge, { role: message.senderRole })
2347
+ /* @__PURE__ */ jsx("span", { style: styles5.authorName, children: message.senderName })
2255
2348
  ] }),
2256
2349
  /* @__PURE__ */ jsxs(
2257
2350
  "div",
@@ -2326,9 +2419,6 @@ function MessageBubble({
2326
2419
  }
2327
2420
  );
2328
2421
  }
2329
- function computeInitials(name) {
2330
- return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
2331
- }
2332
2422
  function roleColor(role) {
2333
2423
  switch (role) {
2334
2424
  case "radiologist":
@@ -2413,9 +2503,6 @@ function SystemBubble({ message }) {
2413
2503
  /* @__PURE__ */ jsx("span", { style: styles5.systemTime, children: formatTime(message.insertedAt) })
2414
2504
  ] }) });
2415
2505
  }
2416
- function RoleBadge({ role }) {
2417
- return /* @__PURE__ */ jsx("span", { style: { ...styles5.roleBadge, backgroundColor: roleColor(role) }, children: ROLE_LABELS[role] ?? role });
2418
- }
2419
2506
  function formatTime(iso) {
2420
2507
  try {
2421
2508
  return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
@@ -2480,7 +2567,7 @@ var styles5 = {
2480
2567
  minWidth: 0
2481
2568
  },
2482
2569
  bubble: {
2483
- padding: `${SPACE.S3} ${SPACE.S4}`,
2570
+ padding: `${SPACE.S2} ${SPACE.S4}`,
2484
2571
  borderRadius: "18px",
2485
2572
  wordBreak: "break-word",
2486
2573
  border: "1px solid transparent"
@@ -2517,16 +2604,6 @@ var styles5 = {
2517
2604
  color: "#fff4d6",
2518
2605
  backgroundColor: "rgba(255, 255, 255, 0.2)"
2519
2606
  },
2520
- roleBadge: {
2521
- fontSize: "10px",
2522
- fontWeight: FONT_WEIGHT.bold,
2523
- padding: "2px 7px",
2524
- borderRadius: RADIUS.sm,
2525
- textTransform: "uppercase",
2526
- letterSpacing: "0.06em",
2527
- color: COLOR.white,
2528
- flexShrink: 0
2529
- },
2530
2607
  textBody: {
2531
2608
  margin: 0,
2532
2609
  fontSize: FONT_SIZE.md,
@@ -2537,7 +2614,7 @@ var styles5 = {
2537
2614
  alignItems: "center",
2538
2615
  justifyContent: "flex-end",
2539
2616
  gap: SPACE.S2,
2540
- marginTop: SPACE.S2,
2617
+ marginTop: SPACE.S1,
2541
2618
  fontSize: "11px",
2542
2619
  opacity: 0.85
2543
2620
  },
@@ -2990,9 +3067,6 @@ var styles6 = {
2990
3067
  alignItems: "center",
2991
3068
  gap: SPACE.S3,
2992
3069
  margin: `${SPACE.S4} 0 ${SPACE.S3}`,
2993
- position: "sticky",
2994
- top: SPACE.S1,
2995
- zIndex: Z_INDEX.sticky,
2996
3070
  pointerEvents: "none"
2997
3071
  },
2998
3072
  dayHr: {
@@ -3007,11 +3081,7 @@ var styles6 = {
3007
3081
  color: COLOR.neutral500,
3008
3082
  textTransform: "uppercase",
3009
3083
  letterSpacing: "0.06em",
3010
- // Subtle pill background so the label is readable when it overlays
3011
- // bubbles passing under it during sticky scroll. Without this the
3012
- // hairlines visually run through the text on tinted backgrounds.
3013
3084
  padding: `2px ${SPACE.S2}`,
3014
- backgroundColor: COLOR.white,
3015
3085
  borderRadius: RADIUS.sm
3016
3086
  },
3017
3087
  bubbleSlot: {
@@ -3309,7 +3379,7 @@ function MessageInput({
3309
3379
  style: styles8.textarea
3310
3380
  }
3311
3381
  ),
3312
- /* @__PURE__ */ jsxs(
3382
+ /* @__PURE__ */ jsx(
3313
3383
  "button",
3314
3384
  {
3315
3385
  onClick: handleSendText,
@@ -3321,10 +3391,7 @@ function MessageInput({
3321
3391
  opacity: text.trim() ? 1 : 0.5
3322
3392
  },
3323
3393
  type: "button",
3324
- children: [
3325
- /* @__PURE__ */ jsx(SendIcon, { size: 20, color: COLOR.white }),
3326
- /* @__PURE__ */ jsx("span", { style: styles8.sendLabel, children: "Send" })
3327
- ]
3394
+ children: /* @__PURE__ */ jsx(SendIcon, { size: 20, color: COLOR.white })
3328
3395
  }
3329
3396
  )
3330
3397
  ] })
@@ -3422,29 +3489,33 @@ var styles8 = {
3422
3489
  fontFamily: "inherit",
3423
3490
  transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease"
3424
3491
  },
3425
- // Pill-shaped send: icon + "Send" label. Always labelled (never icon-
3426
- // only) so the affordance is obvious to first-time users.
3492
+ // Square icon-only send button. The arrow inside an obvious input
3493
+ // context is affordance enough; the word "Send" used to sit next to
3494
+ // it but added clutter without disambiguating anything.
3427
3495
  sendButton: {
3428
3496
  display: "inline-flex",
3429
3497
  alignItems: "center",
3430
- gap: "6px",
3431
- minWidth: "48px",
3498
+ justifyContent: "center",
3499
+ width: "48px",
3432
3500
  height: "48px",
3433
- padding: `0 ${SPACE.S4}`,
3434
3501
  backgroundColor: COLOR.primary,
3435
3502
  color: COLOR.white,
3436
3503
  border: "none",
3437
3504
  borderRadius: "14px",
3438
3505
  cursor: "pointer",
3439
3506
  flexShrink: 0,
3440
- fontFamily: "inherit",
3441
- fontSize: "15px",
3442
- fontWeight: FONT_WEIGHT.semibold,
3443
3507
  transition: "opacity 120ms ease, background-color 120ms ease"
3444
- },
3445
- sendLabel: {
3446
- lineHeight: 1
3447
3508
  }};
3509
+ var ROLE_AVATAR_BG = {
3510
+ radiologist: "#4f46e5",
3511
+ // indigo
3512
+ lab: "#2563eb",
3513
+ // brand blue
3514
+ physician: "#0284c7",
3515
+ // sky
3516
+ admin: "#64748b"
3517
+ // slate
3518
+ };
3448
3519
  function ParticipantsList({
3449
3520
  participants,
3450
3521
  currentUserId,
@@ -3454,7 +3525,17 @@ function ParticipantsList({
3454
3525
  }) {
3455
3526
  return /* @__PURE__ */ jsx("div", { className, style: styles9.list, children: participants.map((participant) => /* @__PURE__ */ jsxs("div", { style: styles9.item, children: [
3456
3527
  /* @__PURE__ */ jsxs("div", { style: styles9.avatar, children: [
3457
- participant.avatar ? /* @__PURE__ */ jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsx("span", { style: styles9.avatarFallback, children: (participant.userName ?? "?").charAt(0).toUpperCase() }),
3528
+ participant.avatar ? /* @__PURE__ */ jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsx(
3529
+ "span",
3530
+ {
3531
+ style: {
3532
+ ...styles9.avatarFallback,
3533
+ backgroundColor: ROLE_AVATAR_BG[participant.userRole]
3534
+ },
3535
+ "aria-hidden": "true",
3536
+ children: /* @__PURE__ */ jsx(RoleAvatarIcon, { role: participant.userRole, size: 18, color: COLOR.white })
3537
+ }
3538
+ ),
3458
3539
  /* @__PURE__ */ jsx(
3459
3540
  "span",
3460
3541
  {
@@ -3470,7 +3551,7 @@ function ParticipantsList({
3470
3551
  participant.userName ?? fallbackName(participant.userRole),
3471
3552
  participant.userId === currentUserId && /* @__PURE__ */ jsx("span", { style: styles9.youBadge, children: " (you)" })
3472
3553
  ] }),
3473
- /* @__PURE__ */ jsx(RoleBadge2, { role: participant.userRole })
3554
+ /* @__PURE__ */ jsx(RoleBadge, { role: participant.userRole })
3474
3555
  ] }),
3475
3556
  isAdmin && participant.userId !== currentUserId && /* @__PURE__ */ jsx(
3476
3557
  "button",
@@ -3498,7 +3579,7 @@ function fallbackName(role) {
3498
3579
  return "User";
3499
3580
  }
3500
3581
  }
3501
- function RoleBadge2({ role }) {
3582
+ function RoleBadge({ role }) {
3502
3583
  const colors = {
3503
3584
  radiologist: { bg: "#eef2ff", text: "#4f46e5" },
3504
3585
  // indigo
@@ -3540,13 +3621,10 @@ var styles9 = {
3540
3621
  width: "32px",
3541
3622
  height: "32px",
3542
3623
  borderRadius: RADIUS.full,
3543
- backgroundColor: COLOR.neutral200,
3544
3624
  display: "flex",
3545
3625
  alignItems: "center",
3546
3626
  justifyContent: "center",
3547
- fontSize: FONT_SIZE.sm,
3548
- fontWeight: FONT_WEIGHT.semibold,
3549
- color: COLOR.neutral500
3627
+ color: COLOR.white
3550
3628
  },
3551
3629
  statusDot: {
3552
3630
  position: "absolute",
@@ -4138,13 +4216,19 @@ function CollabPanel({
4138
4216
  showSeenBy = true,
4139
4217
  onBack,
4140
4218
  hidePatientName = false,
4219
+ hideOpenCase = false,
4141
4220
  onConversationChange,
4142
4221
  themeMode = "light",
4222
+ showSettings: controlledShowSettings,
4223
+ onSettingsChange,
4143
4224
  className,
4144
4225
  style
4145
4226
  }) {
4146
4227
  const { config } = useCollab();
4147
- const [showSettings, setShowSettings] = useState(false);
4228
+ const isSettingsControlled = controlledShowSettings !== void 0 && onSettingsChange !== void 0;
4229
+ const [internalShowSettings, setInternalShowSettings] = useState(false);
4230
+ const showSettings = isSettingsControlled ? controlledShowSettings : internalShowSettings;
4231
+ const updateShowSettings = isSettingsControlled ? onSettingsChange : setInternalShowSettings;
4148
4232
  const [toast, setToast] = useState(null);
4149
4233
  const messageListRef = useRef(null);
4150
4234
  const {
@@ -4180,6 +4264,7 @@ function CollabPanel({
4180
4264
  config.onOpenDicom(patientData.studyId, patientData.storageId);
4181
4265
  }
4182
4266
  };
4267
+ const handleOpenCase = !hideOpenCase && config.onOpenCase ? () => config.onOpenCase?.(patientData.orderId, patientData.displayOrderId) : void 0;
4183
4268
  const handleJumpToMessage = (messageId) => {
4184
4269
  messageListRef.current?.scrollToMessage(messageId);
4185
4270
  };
@@ -4246,7 +4331,7 @@ function CollabPanel({
4246
4331
  onRemoveUser: channelSettings.removeUser,
4247
4332
  onLeaveChannel: channelSettings.leaveChannel,
4248
4333
  onDeleteChannel: channelSettings.deleteChannel,
4249
- onClose: () => setShowSettings(false)
4334
+ onClose: () => updateShowSettings(false)
4250
4335
  }
4251
4336
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
4252
4337
  /* @__PURE__ */ jsx(
@@ -4255,7 +4340,8 @@ function CollabPanel({
4255
4340
  patientData,
4256
4341
  participants,
4257
4342
  onOpenDicom: handleOpenDicom,
4258
- onOpenSettings: () => setShowSettings(true),
4343
+ onOpenCase: handleOpenCase,
4344
+ onOpenSettings: hidePatientName ? void 0 : () => updateShowSettings(true),
4259
4345
  onBack,
4260
4346
  hideName: hidePatientName,
4261
4347
  displayName: conversation?.name
@@ -4398,6 +4484,7 @@ function CollabPopup({
4398
4484
  );
4399
4485
  const [isDragging, setIsDragging] = useState(false);
4400
4486
  const [isMinimized, setIsMinimized] = useState(false);
4487
+ const [showSettings, setShowSettings] = useState(false);
4401
4488
  const [loadedConversation, setLoadedConversation] = useState(null);
4402
4489
  const dragOffset = useRef({ x: 0, y: 0 });
4403
4490
  const containerRef = useRef(null);
@@ -4509,16 +4596,32 @@ function CollabPopup({
4509
4596
  ),
4510
4597
  /* @__PURE__ */ jsxs("div", { style: styles13.titleCenter, children: [
4511
4598
  /* @__PURE__ */ jsx("h2", { id: titleId, style: styles13.patientName, children: patientData.patientName || titleText }),
4512
- (patientData.labName || patientData.displayOrderId) && /* @__PURE__ */ jsxs("div", { style: styles13.subRow, children: [
4513
- patientData.labName && /* @__PURE__ */ jsx("span", { children: patientData.labName }),
4514
- patientData.labName && patientData.displayOrderId && /* @__PURE__ */ jsx("span", { style: styles13.subDot, "aria-hidden": "true", children: "\xB7" }),
4515
- patientData.displayOrderId && /* @__PURE__ */ jsxs("span", { style: styles13.mono, children: [
4516
- "#",
4517
- patientData.displayOrderId.slice(-4)
4518
- ] })
4519
- ] })
4599
+ (() => {
4600
+ const parts = [];
4601
+ if (patientData.patientAge) parts.push(String(patientData.patientAge));
4602
+ if (patientData.patientSex) parts.push(String(patientData.patientSex));
4603
+ if (patientData.studyType) parts.push(patientData.studyType);
4604
+ if (patientData.bodyParts && patientData.bodyParts.length > 0) {
4605
+ parts.push(patientData.bodyParts.join(", "));
4606
+ }
4607
+ return parts.length > 0 ? /* @__PURE__ */ jsx("div", { style: styles13.subRow, children: /* @__PURE__ */ jsx("span", { style: styles13.metaText, children: parts.join(" \xB7 ") }) }) : null;
4608
+ })()
4520
4609
  ] }),
4521
4610
  /* @__PURE__ */ jsxs("div", { style: styles13.titleActions, children: [
4611
+ loadedConversation && /* @__PURE__ */ jsxs(
4612
+ "button",
4613
+ {
4614
+ type: "button",
4615
+ onClick: () => setShowSettings(true),
4616
+ style: styles13.titleSettingsPill,
4617
+ "aria-label": `Open channel settings (${loadedConversation.participants.length} participants)`,
4618
+ title: "Channel participants",
4619
+ children: [
4620
+ /* @__PURE__ */ jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
4621
+ /* @__PURE__ */ jsx("span", { style: styles13.titleParticipantCount, children: loadedConversation.participants.length })
4622
+ ]
4623
+ }
4624
+ ),
4522
4625
  /* @__PURE__ */ jsx(
4523
4626
  "button",
4524
4627
  {
@@ -4553,7 +4656,9 @@ function CollabPopup({
4553
4656
  patientData,
4554
4657
  participantIds,
4555
4658
  hidePatientName: true,
4556
- onConversationChange: setLoadedConversation
4659
+ onConversationChange: setLoadedConversation,
4660
+ showSettings,
4661
+ onSettingsChange: setShowSettings
4557
4662
  }
4558
4663
  ) })
4559
4664
  ]
@@ -4605,19 +4710,17 @@ var styles13 = {
4605
4710
  subRow: {
4606
4711
  display: "flex",
4607
4712
  alignItems: "center",
4608
- gap: SPACE.S2,
4609
4713
  fontSize: FONT_SIZE.sm,
4610
4714
  color: COLOR.neutral500,
4611
4715
  overflow: "hidden",
4612
4716
  textOverflow: "ellipsis",
4613
4717
  whiteSpace: "nowrap"
4614
4718
  },
4615
- subDot: {
4616
- color: COLOR.neutral400
4617
- },
4618
- mono: {
4619
- fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4620
- fontVariantNumeric: "tabular-nums"
4719
+ metaText: {
4720
+ overflow: "hidden",
4721
+ textOverflow: "ellipsis",
4722
+ whiteSpace: "nowrap",
4723
+ minWidth: 0
4621
4724
  },
4622
4725
  titleActions: {
4623
4726
  display: "flex",
@@ -4637,6 +4740,29 @@ var styles13 = {
4637
4740
  color: COLOR.neutral700,
4638
4741
  flexShrink: 0
4639
4742
  },
4743
+ // People-icon + participant-count pill in the title-actions row.
4744
+ // Matches the in-panel settings button's shape (icon + count) so a
4745
+ // user moving between popup mode and standalone mode (CollabInbox
4746
+ // compact) sees the same control language.
4747
+ titleSettingsPill: {
4748
+ height: "36px",
4749
+ display: "inline-flex",
4750
+ alignItems: "center",
4751
+ gap: SPACE.S2,
4752
+ padding: `0 ${SPACE.S3}`,
4753
+ backgroundColor: COLOR.neutral100,
4754
+ border: `1px solid ${COLOR.neutral200}`,
4755
+ borderRadius: RADIUS.md,
4756
+ color: COLOR.neutral600,
4757
+ cursor: "pointer",
4758
+ flexShrink: 0
4759
+ },
4760
+ titleParticipantCount: {
4761
+ fontSize: FONT_SIZE.sm,
4762
+ fontWeight: FONT_WEIGHT.semibold,
4763
+ color: COLOR.neutral700,
4764
+ fontVariantNumeric: "tabular-nums"
4765
+ },
4640
4766
  panelWrapper: {
4641
4767
  flex: 1,
4642
4768
  overflow: "hidden"
@@ -5430,7 +5556,7 @@ function ConversationListItem({
5430
5556
  const studyType = item.patientSnapshot?.studyType;
5431
5557
  const patientId = item.patientSnapshot?.patientId;
5432
5558
  const showStudyRow = !!(studyType || patientId);
5433
- const initials = computeInitials2(item.patientSnapshot?.patientName || displayName);
5559
+ const initials = computeInitials(item.patientSnapshot?.patientName || displayName);
5434
5560
  return /* @__PURE__ */ jsxs(
5435
5561
  "button",
5436
5562
  {
@@ -5525,7 +5651,7 @@ function previewText(message) {
5525
5651
  }
5526
5652
  }
5527
5653
  }
5528
- function computeInitials2(name) {
5654
+ function computeInitials(name) {
5529
5655
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("") || "?";
5530
5656
  }
5531
5657
  var styles15 = {