@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.js CHANGED
@@ -519,8 +519,6 @@ var SHADOW = {
519
519
  toast: "0 4px 12px rgba(0, 0, 0, 0.18)"
520
520
  };
521
521
  var Z_INDEX = {
522
- /** Sticky day divider — above bubbles, below interactive popovers. */
523
- sticky: 1,
524
522
  /** Toasts inside a panel. */
525
523
  toast: 20,
526
524
  /** Floating CollabPopup dialog. */
@@ -1452,6 +1450,31 @@ function DicomIcon({ size = 18, color = "currentColor" }) {
1452
1450
  }
1453
1451
  );
1454
1452
  }
1453
+ function OpenCaseIcon({
1454
+ size = 18,
1455
+ color = "currentColor"
1456
+ }) {
1457
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1458
+ "svg",
1459
+ {
1460
+ xmlns: "http://www.w3.org/2000/svg",
1461
+ width: size,
1462
+ height: size,
1463
+ viewBox: "0 0 24 24",
1464
+ fill: "none",
1465
+ stroke: color,
1466
+ strokeWidth: "2",
1467
+ strokeLinecap: "round",
1468
+ strokeLinejoin: "round",
1469
+ "aria-hidden": "true",
1470
+ children: [
1471
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 4h6v6" }),
1472
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 4L11 13" }),
1473
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5" })
1474
+ ]
1475
+ }
1476
+ );
1477
+ }
1455
1478
  function PeopleIcon({ size = 18, color = "currentColor" }) {
1456
1479
  return /* @__PURE__ */ jsxRuntime.jsxs(
1457
1480
  "svg",
@@ -1489,15 +1512,19 @@ function BackIcon({ size = 22, color = "currentColor" }) {
1489
1512
  }
1490
1513
  );
1491
1514
  }
1515
+ function stripOrderIdSuffix(name) {
1516
+ return name.replace(/\s*-\s*\d{1,6}\s*$/, "").trim();
1517
+ }
1492
1518
  function resolveDisplayName(patientData, displayName) {
1493
1519
  const localComplete = !!patientData.labName && !!patientData.displayOrderId;
1494
- if (localComplete) return buildChannelName(patientData);
1495
- return cleanChannelName(displayName) || buildChannelName(patientData);
1520
+ const raw = localComplete ? buildChannelName(patientData) : cleanChannelName(displayName) || buildChannelName(patientData);
1521
+ return stripOrderIdSuffix(raw);
1496
1522
  }
1497
1523
  function PatientHeader({
1498
1524
  patientData,
1499
1525
  participants,
1500
1526
  onOpenDicom,
1527
+ onOpenCase,
1501
1528
  onOpenSettings,
1502
1529
  onBack,
1503
1530
  hideName = false,
@@ -1505,29 +1532,58 @@ function PatientHeader({
1505
1532
  className
1506
1533
  }) {
1507
1534
  const hasDicom = !!(patientData.studyId && patientData.storageId);
1508
- const meta = [];
1509
- if (patientData.patientAge && patientData.patientSex) {
1510
- meta.push({
1511
- key: "ageSex",
1512
- label: "Age / Sex",
1513
- value: `${patientData.patientAge} \xB7 ${patientData.patientSex}`
1514
- });
1515
- } else if (patientData.patientAge) {
1516
- meta.push({ key: "age", label: "Age", value: String(patientData.patientAge) });
1517
- } else if (patientData.patientSex) {
1518
- meta.push({ key: "sex", label: "Sex", value: String(patientData.patientSex) });
1519
- }
1520
- if (patientData.studyType) {
1521
- meta.push({ key: "modality", label: "Modality", value: patientData.studyType });
1522
- }
1535
+ const resolvedName = resolveDisplayName(patientData, displayName);
1536
+ const metaParts = [];
1537
+ if (patientData.patientAge) metaParts.push(String(patientData.patientAge));
1538
+ if (patientData.patientSex) metaParts.push(String(patientData.patientSex));
1539
+ if (patientData.studyType) metaParts.push(patientData.studyType);
1523
1540
  if (patientData.bodyParts && patientData.bodyParts.length > 0) {
1524
- meta.push({
1525
- key: "body",
1526
- label: "Body part",
1527
- value: patientData.bodyParts.join(", ")
1528
- });
1541
+ metaParts.push(patientData.bodyParts.join(", "));
1529
1542
  }
1530
- const hasActions = hasDicom && onOpenDicom || onOpenSettings;
1543
+ const hasPrimaryActions = hasDicom && onOpenDicom || onOpenCase;
1544
+ const primaryActions = hasPrimaryActions ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actions, children: [
1545
+ hasDicom && onOpenDicom && /* @__PURE__ */ jsxRuntime.jsxs(
1546
+ "button",
1547
+ {
1548
+ onClick: onOpenDicom,
1549
+ style: styles.dicomButton,
1550
+ type: "button",
1551
+ "aria-label": "View DICOM study",
1552
+ children: [
1553
+ /* @__PURE__ */ jsxRuntime.jsx(DicomIcon, { size: 18, color: COLOR.primary }),
1554
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "View DICOM" })
1555
+ ]
1556
+ }
1557
+ ),
1558
+ onOpenCase && /* @__PURE__ */ jsxRuntime.jsxs(
1559
+ "button",
1560
+ {
1561
+ onClick: onOpenCase,
1562
+ style: styles.openCaseButton,
1563
+ type: "button",
1564
+ "aria-label": "Open case",
1565
+ title: "Open case",
1566
+ children: [
1567
+ /* @__PURE__ */ jsxRuntime.jsx(OpenCaseIcon, { size: 18, color: COLOR.primary }),
1568
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Open" })
1569
+ ]
1570
+ }
1571
+ )
1572
+ ] }) : null;
1573
+ const settingsButton = onOpenSettings ? /* @__PURE__ */ jsxRuntime.jsxs(
1574
+ "button",
1575
+ {
1576
+ onClick: onOpenSettings,
1577
+ style: styles.settingsIconButton,
1578
+ type: "button",
1579
+ "aria-label": `Open channel settings (${participants.length} participants)`,
1580
+ title: "Channel participants",
1581
+ children: [
1582
+ /* @__PURE__ */ jsxRuntime.jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
1583
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.participantCount, children: participants.length })
1584
+ ]
1585
+ }
1586
+ ) : null;
1531
1587
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles.container, children: [
1532
1588
  !hideName && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.nameRow, children: [
1533
1589
  onBack && /* @__PURE__ */ jsxRuntime.jsx(
@@ -1538,45 +1594,19 @@ function PatientHeader({
1538
1594
  style: styles.backButton,
1539
1595
  "aria-label": "Back to conversations",
1540
1596
  title: "Back to conversations",
1541
- children: /* @__PURE__ */ jsxRuntime.jsx(BackIcon, { size: 22, color: COLOR.neutral800 })
1597
+ children: /* @__PURE__ */ jsxRuntime.jsx(BackIcon, { size: 20, color: COLOR.neutral800 })
1542
1598
  }
1543
1599
  ),
1544
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.nameText, children: resolveDisplayName(patientData, displayName) })
1600
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.nameText, children: resolvedName }),
1601
+ primaryActions
1545
1602
  ] }),
1546
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.caseCard, children: [
1547
- meta.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.metaRow, children: meta.map((item) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.metaCol, children: [
1548
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.metaLabel, children: item.label }),
1549
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.metaValue, children: item.value })
1550
- ] }, item.key)) }),
1551
- hasActions && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actions, children: [
1552
- hasDicom && onOpenDicom && /* @__PURE__ */ jsxRuntime.jsxs(
1553
- "button",
1554
- {
1555
- onClick: onOpenDicom,
1556
- style: styles.dicomButton,
1557
- type: "button",
1558
- "aria-label": "View DICOM study",
1559
- children: [
1560
- /* @__PURE__ */ jsxRuntime.jsx(DicomIcon, { size: 18, color: COLOR.primary }),
1561
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: "View DICOM" })
1562
- ]
1563
- }
1564
- ),
1565
- onOpenSettings && /* @__PURE__ */ jsxRuntime.jsxs(
1566
- "button",
1567
- {
1568
- onClick: onOpenSettings,
1569
- style: styles.settingsIconButton,
1570
- type: "button",
1571
- "aria-label": `Open channel settings (${participants.length} participants)`,
1572
- title: "Channel participants",
1573
- children: [
1574
- /* @__PURE__ */ jsxRuntime.jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
1575
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.participantCount, children: participants.length })
1576
- ]
1577
- }
1578
- )
1579
- ] })
1603
+ !hideName && (metaParts.length > 0 || settingsButton) && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.metaRow, children: [
1604
+ metaParts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.metaText, children: metaParts.join(" \xB7 ") }),
1605
+ settingsButton && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.metaRowEnd, children: settingsButton })
1606
+ ] }),
1607
+ hideName && (primaryActions || settingsButton) && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actionsOnlyRow, children: [
1608
+ primaryActions,
1609
+ settingsButton
1580
1610
  ] })
1581
1611
  ] });
1582
1612
  }
@@ -1585,16 +1615,16 @@ var styles = {
1585
1615
  display: "flex",
1586
1616
  flexDirection: "column",
1587
1617
  backgroundColor: COLOR.primaryBg,
1588
- borderBottom: `1px solid ${COLOR.neutral200}`
1618
+ borderBottom: `1px solid ${COLOR.neutral200}`,
1619
+ padding: `${SPACE.S2} ${SPACE.S4}`,
1620
+ gap: "2px"
1589
1621
  },
1590
- // Optional name row, only when `hideName` is false (compact-inbox path)
1622
+ // Title row back · name · actions in one horizontal line.
1591
1623
  nameRow: {
1592
1624
  display: "flex",
1593
1625
  alignItems: "center",
1594
1626
  gap: SPACE.S2,
1595
- padding: `${SPACE.S3} ${SPACE.S4} ${SPACE.S2}`,
1596
- borderBottom: `1px solid ${COLOR.neutral200}`,
1597
- backgroundColor: COLOR.white
1627
+ minWidth: 0
1598
1628
  },
1599
1629
  backButton: {
1600
1630
  width: SIZE.controlIcon,
@@ -1610,57 +1640,51 @@ var styles = {
1610
1640
  flexShrink: 0
1611
1641
  },
1612
1642
  nameText: {
1643
+ flex: 1,
1644
+ minWidth: 0,
1613
1645
  fontSize: FONT_SIZE.lg,
1614
1646
  fontWeight: FONT_WEIGHT.bold,
1615
1647
  color: COLOR.neutral900,
1616
1648
  lineHeight: LINE_HEIGHT.tight,
1617
1649
  letterSpacing: "-0.01em",
1618
- minWidth: 0,
1619
1650
  overflow: "hidden",
1620
1651
  textOverflow: "ellipsis",
1621
1652
  whiteSpace: "nowrap"
1622
1653
  },
1623
- // Case card: meta columns flowing in a wrap-row, actions stacked
1624
- // below. Each meta column is a small label-above-value pair so the
1625
- // user can scan field names quickly without a legend.
1626
- caseCard: {
1627
- display: "flex",
1628
- flexDirection: "column",
1629
- gap: SPACE.S3,
1630
- padding: `${SPACE.S3} ${SPACE.S5}`
1631
- },
1632
- // Wrap-row of stacked label/value columns. Compact column gap keeps
1633
- // the row dense without crowding; row gap kicks in when columns
1634
- // wrap to a second line on narrow popups.
1654
+ // Sub-row beneath the title small muted meta on the left, the
1655
+ // channel-settings pill on the right when present. Meta text can
1656
+ // ellipsis on narrow widths; the settings pill stays its content
1657
+ // width and hugs the trailing edge via `metaRowEnd`.
1635
1658
  metaRow: {
1636
1659
  display: "flex",
1637
- flexWrap: "wrap",
1638
- columnGap: SPACE.S5,
1639
- rowGap: SPACE.S2,
1660
+ alignItems: "center",
1661
+ gap: SPACE.S2,
1662
+ paddingLeft: `calc(${SIZE.controlIcon} + ${SPACE.S2} - ${SPACE.S2})`,
1640
1663
  minWidth: 0
1641
1664
  },
1642
- metaCol: {
1665
+ metaRowEnd: {
1666
+ marginLeft: "auto",
1667
+ flexShrink: 0
1668
+ },
1669
+ // When hideName=true (popup mode), only the actions render. The
1670
+ // popup's own title bar carries the name + meta above this row, so
1671
+ // the actions land flush-left here — pinning them to the right
1672
+ // edge would leave the whole left half of the strip awkwardly
1673
+ // empty (the meta that used to fill it now lives in the title bar).
1674
+ actionsOnlyRow: {
1643
1675
  display: "flex",
1644
- flexDirection: "column",
1645
- gap: "2px",
1676
+ justifyContent: "flex-start",
1646
1677
  minWidth: 0
1647
1678
  },
1648
- metaLabel: {
1649
- fontSize: "11px",
1650
- fontWeight: FONT_WEIGHT.bold,
1651
- letterSpacing: "0.06em",
1652
- textTransform: "uppercase",
1653
- color: COLOR.neutral500,
1654
- whiteSpace: "nowrap"
1655
- },
1656
- metaValue: {
1679
+ metaText: {
1657
1680
  fontSize: FONT_SIZE.sm,
1658
- fontWeight: FONT_WEIGHT.semibold,
1659
- color: COLOR.neutral900,
1660
- whiteSpace: "nowrap",
1681
+ fontWeight: FONT_WEIGHT.medium,
1682
+ color: COLOR.neutral600,
1683
+ lineHeight: LINE_HEIGHT.tight,
1661
1684
  overflow: "hidden",
1662
1685
  textOverflow: "ellipsis",
1663
- maxWidth: "180px"
1686
+ whiteSpace: "nowrap",
1687
+ minWidth: 0
1664
1688
  },
1665
1689
  actions: {
1666
1690
  display: "flex",
@@ -1672,7 +1696,24 @@ var styles = {
1672
1696
  alignItems: "center",
1673
1697
  gap: SPACE.S2,
1674
1698
  minHeight: SIZE.control,
1675
- padding: `${SPACE.S2} 14px`,
1699
+ padding: `${SPACE.S1} ${SPACE.S3}`,
1700
+ fontSize: FONT_SIZE.sm,
1701
+ fontWeight: FONT_WEIGHT.semibold,
1702
+ color: COLOR.primary,
1703
+ backgroundColor: COLOR.white,
1704
+ border: `1.5px solid ${COLOR.primary}`,
1705
+ borderRadius: RADIUS.lg,
1706
+ cursor: "pointer"
1707
+ },
1708
+ // Sibling of the DICOM button — same shape so the two read as a pair.
1709
+ // Click hands off to the host's `onOpenCase` callback (role-aware
1710
+ // routing lives on the host side).
1711
+ openCaseButton: {
1712
+ display: "inline-flex",
1713
+ alignItems: "center",
1714
+ gap: SPACE.S2,
1715
+ minHeight: SIZE.control,
1716
+ padding: `${SPACE.S1} ${SPACE.S3}`,
1676
1717
  fontSize: FONT_SIZE.sm,
1677
1718
  fontWeight: FONT_WEIGHT.semibold,
1678
1719
  color: COLOR.primary,
@@ -1681,10 +1722,8 @@ var styles = {
1681
1722
  borderRadius: RADIUS.lg,
1682
1723
  cursor: "pointer"
1683
1724
  },
1684
- // Channel-info button: people icon + participant count. Click opens the
1685
- // channel settings overlay (kept on the same handler as before so hosts
1686
- // don't have to re-wire — the affordance just looks like a "members"
1687
- // pill now instead of a gear).
1725
+ // Channel-info button: people icon + participant count. Opens the
1726
+ // channel settings overlay.
1688
1727
  settingsIconButton: {
1689
1728
  minHeight: SIZE.control,
1690
1729
  display: "inline-flex",
@@ -1827,7 +1866,6 @@ var styles3 = {
1827
1866
  display: "flex",
1828
1867
  alignItems: "center",
1829
1868
  gap: SPACE.S1,
1830
- marginTop: "2px",
1831
1869
  fontSize: FONT_SIZE.xs,
1832
1870
  opacity: 0.85
1833
1871
  },
@@ -2164,6 +2202,62 @@ function AlertIcon({ size = 14, color = "currentColor" }) {
2164
2202
  }
2165
2203
  );
2166
2204
  }
2205
+ function RoleAvatarIcon({
2206
+ role,
2207
+ size = 18,
2208
+ color = "currentColor",
2209
+ className
2210
+ }) {
2211
+ const common = {
2212
+ width: size,
2213
+ height: size,
2214
+ viewBox: "0 0 24 24",
2215
+ fill: "none",
2216
+ stroke: color,
2217
+ strokeWidth: 1.9,
2218
+ strokeLinecap: "round",
2219
+ strokeLinejoin: "round",
2220
+ "aria-hidden": true,
2221
+ className
2222
+ };
2223
+ switch (role) {
2224
+ case "radiologist":
2225
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...common, children: [
2226
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.5 8.5h9" }),
2227
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "9.5", r: "4" }),
2228
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "7.5", r: "1", fill: color, stroke: "none" }),
2229
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 21v-1.5a5 5 0 0 1 5-5h6a5 5 0 0 1 5 5V21" })
2230
+ ] });
2231
+ case "lab":
2232
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...common, children: [
2233
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21V9l9-6 9 6v12" }),
2234
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 11v6" }),
2235
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 14h6" }),
2236
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M11 21v-3h2v3" }),
2237
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21h18" })
2238
+ ] });
2239
+ case "physician":
2240
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...common, children: [
2241
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 3v5a4 4 0 0 0 8 0V3" }),
2242
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 3h2" }),
2243
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 3h2" }),
2244
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M11 12v3a5 5 0 0 0 5 5h0a5 5 0 0 0 5-5v-2" }),
2245
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "21", cy: "11", r: "2" })
2246
+ ] });
2247
+ case "admin":
2248
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...common, children: [
2249
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3" }),
2250
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2v3" }),
2251
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19v3" }),
2252
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 12H2" }),
2253
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22 12h-3" }),
2254
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5.6 5.6l2 2" }),
2255
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.4 16.4l2 2" }),
2256
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5.6 18.4l2-2" }),
2257
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.4 7.6l2-2" })
2258
+ ] });
2259
+ }
2260
+ }
2167
2261
  function Spinner({ size = 16, color = COLOR.neutral500, thickness = 2 }) {
2168
2262
  return /* @__PURE__ */ jsxRuntime.jsx(
2169
2263
  "span",
@@ -2252,12 +2346,11 @@ function MessageBubble({
2252
2346
  ...styles5.authorAvatar,
2253
2347
  backgroundColor: roleColor(message.senderRole)
2254
2348
  },
2255
- "aria-hidden": "true",
2256
- children: computeInitials(message.senderName)
2349
+ "aria-label": ROLE_LABELS[message.senderRole] ?? message.senderRole,
2350
+ children: /* @__PURE__ */ jsxRuntime.jsx(RoleAvatarIcon, { role: message.senderRole, size: 20, color: COLOR.white })
2257
2351
  }
2258
2352
  ),
2259
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.authorName, children: message.senderName }),
2260
- /* @__PURE__ */ jsxRuntime.jsx(RoleBadge, { role: message.senderRole })
2353
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.authorName, children: message.senderName })
2261
2354
  ] }),
2262
2355
  /* @__PURE__ */ jsxRuntime.jsxs(
2263
2356
  "div",
@@ -2332,9 +2425,6 @@ function MessageBubble({
2332
2425
  }
2333
2426
  );
2334
2427
  }
2335
- function computeInitials(name) {
2336
- return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
2337
- }
2338
2428
  function roleColor(role) {
2339
2429
  switch (role) {
2340
2430
  case "radiologist":
@@ -2419,9 +2509,6 @@ function SystemBubble({ message }) {
2419
2509
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.systemTime, children: formatTime(message.insertedAt) })
2420
2510
  ] }) });
2421
2511
  }
2422
- function RoleBadge({ role }) {
2423
- return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles5.roleBadge, backgroundColor: roleColor(role) }, children: ROLE_LABELS[role] ?? role });
2424
- }
2425
2512
  function formatTime(iso) {
2426
2513
  try {
2427
2514
  return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
@@ -2486,7 +2573,7 @@ var styles5 = {
2486
2573
  minWidth: 0
2487
2574
  },
2488
2575
  bubble: {
2489
- padding: `${SPACE.S3} ${SPACE.S4}`,
2576
+ padding: `${SPACE.S2} ${SPACE.S4}`,
2490
2577
  borderRadius: "18px",
2491
2578
  wordBreak: "break-word",
2492
2579
  border: "1px solid transparent"
@@ -2523,16 +2610,6 @@ var styles5 = {
2523
2610
  color: "#fff4d6",
2524
2611
  backgroundColor: "rgba(255, 255, 255, 0.2)"
2525
2612
  },
2526
- roleBadge: {
2527
- fontSize: "10px",
2528
- fontWeight: FONT_WEIGHT.bold,
2529
- padding: "2px 7px",
2530
- borderRadius: RADIUS.sm,
2531
- textTransform: "uppercase",
2532
- letterSpacing: "0.06em",
2533
- color: COLOR.white,
2534
- flexShrink: 0
2535
- },
2536
2613
  textBody: {
2537
2614
  margin: 0,
2538
2615
  fontSize: FONT_SIZE.md,
@@ -2543,7 +2620,7 @@ var styles5 = {
2543
2620
  alignItems: "center",
2544
2621
  justifyContent: "flex-end",
2545
2622
  gap: SPACE.S2,
2546
- marginTop: SPACE.S2,
2623
+ marginTop: SPACE.S1,
2547
2624
  fontSize: "11px",
2548
2625
  opacity: 0.85
2549
2626
  },
@@ -2996,9 +3073,6 @@ var styles6 = {
2996
3073
  alignItems: "center",
2997
3074
  gap: SPACE.S3,
2998
3075
  margin: `${SPACE.S4} 0 ${SPACE.S3}`,
2999
- position: "sticky",
3000
- top: SPACE.S1,
3001
- zIndex: Z_INDEX.sticky,
3002
3076
  pointerEvents: "none"
3003
3077
  },
3004
3078
  dayHr: {
@@ -3013,11 +3087,7 @@ var styles6 = {
3013
3087
  color: COLOR.neutral500,
3014
3088
  textTransform: "uppercase",
3015
3089
  letterSpacing: "0.06em",
3016
- // Subtle pill background so the label is readable when it overlays
3017
- // bubbles passing under it during sticky scroll. Without this the
3018
- // hairlines visually run through the text on tinted backgrounds.
3019
3090
  padding: `2px ${SPACE.S2}`,
3020
- backgroundColor: COLOR.white,
3021
3091
  borderRadius: RADIUS.sm
3022
3092
  },
3023
3093
  bubbleSlot: {
@@ -3315,7 +3385,7 @@ function MessageInput({
3315
3385
  style: styles8.textarea
3316
3386
  }
3317
3387
  ),
3318
- /* @__PURE__ */ jsxRuntime.jsxs(
3388
+ /* @__PURE__ */ jsxRuntime.jsx(
3319
3389
  "button",
3320
3390
  {
3321
3391
  onClick: handleSendText,
@@ -3327,10 +3397,7 @@ function MessageInput({
3327
3397
  opacity: text.trim() ? 1 : 0.5
3328
3398
  },
3329
3399
  type: "button",
3330
- children: [
3331
- /* @__PURE__ */ jsxRuntime.jsx(SendIcon, { size: 20, color: COLOR.white }),
3332
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles8.sendLabel, children: "Send" })
3333
- ]
3400
+ children: /* @__PURE__ */ jsxRuntime.jsx(SendIcon, { size: 20, color: COLOR.white })
3334
3401
  }
3335
3402
  )
3336
3403
  ] })
@@ -3428,29 +3495,33 @@ var styles8 = {
3428
3495
  fontFamily: "inherit",
3429
3496
  transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease"
3430
3497
  },
3431
- // Pill-shaped send: icon + "Send" label. Always labelled (never icon-
3432
- // only) so the affordance is obvious to first-time users.
3498
+ // Square icon-only send button. The arrow inside an obvious input
3499
+ // context is affordance enough; the word "Send" used to sit next to
3500
+ // it but added clutter without disambiguating anything.
3433
3501
  sendButton: {
3434
3502
  display: "inline-flex",
3435
3503
  alignItems: "center",
3436
- gap: "6px",
3437
- minWidth: "48px",
3504
+ justifyContent: "center",
3505
+ width: "48px",
3438
3506
  height: "48px",
3439
- padding: `0 ${SPACE.S4}`,
3440
3507
  backgroundColor: COLOR.primary,
3441
3508
  color: COLOR.white,
3442
3509
  border: "none",
3443
3510
  borderRadius: "14px",
3444
3511
  cursor: "pointer",
3445
3512
  flexShrink: 0,
3446
- fontFamily: "inherit",
3447
- fontSize: "15px",
3448
- fontWeight: FONT_WEIGHT.semibold,
3449
3513
  transition: "opacity 120ms ease, background-color 120ms ease"
3450
- },
3451
- sendLabel: {
3452
- lineHeight: 1
3453
3514
  }};
3515
+ var ROLE_AVATAR_BG = {
3516
+ radiologist: "#4f46e5",
3517
+ // indigo
3518
+ lab: "#2563eb",
3519
+ // brand blue
3520
+ physician: "#0284c7",
3521
+ // sky
3522
+ admin: "#64748b"
3523
+ // slate
3524
+ };
3454
3525
  function ParticipantsList({
3455
3526
  participants,
3456
3527
  currentUserId,
@@ -3460,7 +3531,17 @@ function ParticipantsList({
3460
3531
  }) {
3461
3532
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: styles9.list, children: participants.map((participant) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles9.item, children: [
3462
3533
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles9.avatar, children: [
3463
- participant.avatar ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles9.avatarFallback, children: (participant.userName ?? "?").charAt(0).toUpperCase() }),
3534
+ participant.avatar ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsxRuntime.jsx(
3535
+ "span",
3536
+ {
3537
+ style: {
3538
+ ...styles9.avatarFallback,
3539
+ backgroundColor: ROLE_AVATAR_BG[participant.userRole]
3540
+ },
3541
+ "aria-hidden": "true",
3542
+ children: /* @__PURE__ */ jsxRuntime.jsx(RoleAvatarIcon, { role: participant.userRole, size: 18, color: COLOR.white })
3543
+ }
3544
+ ),
3464
3545
  /* @__PURE__ */ jsxRuntime.jsx(
3465
3546
  "span",
3466
3547
  {
@@ -3476,7 +3557,7 @@ function ParticipantsList({
3476
3557
  participant.userName ?? fallbackName(participant.userRole),
3477
3558
  participant.userId === currentUserId && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles9.youBadge, children: " (you)" })
3478
3559
  ] }),
3479
- /* @__PURE__ */ jsxRuntime.jsx(RoleBadge2, { role: participant.userRole })
3560
+ /* @__PURE__ */ jsxRuntime.jsx(RoleBadge, { role: participant.userRole })
3480
3561
  ] }),
3481
3562
  isAdmin && participant.userId !== currentUserId && /* @__PURE__ */ jsxRuntime.jsx(
3482
3563
  "button",
@@ -3504,7 +3585,7 @@ function fallbackName(role) {
3504
3585
  return "User";
3505
3586
  }
3506
3587
  }
3507
- function RoleBadge2({ role }) {
3588
+ function RoleBadge({ role }) {
3508
3589
  const colors = {
3509
3590
  radiologist: { bg: "#eef2ff", text: "#4f46e5" },
3510
3591
  // indigo
@@ -3546,13 +3627,10 @@ var styles9 = {
3546
3627
  width: "32px",
3547
3628
  height: "32px",
3548
3629
  borderRadius: RADIUS.full,
3549
- backgroundColor: COLOR.neutral200,
3550
3630
  display: "flex",
3551
3631
  alignItems: "center",
3552
3632
  justifyContent: "center",
3553
- fontSize: FONT_SIZE.sm,
3554
- fontWeight: FONT_WEIGHT.semibold,
3555
- color: COLOR.neutral500
3633
+ color: COLOR.white
3556
3634
  },
3557
3635
  statusDot: {
3558
3636
  position: "absolute",
@@ -4144,13 +4222,19 @@ function CollabPanel({
4144
4222
  showSeenBy = true,
4145
4223
  onBack,
4146
4224
  hidePatientName = false,
4225
+ hideOpenCase = false,
4147
4226
  onConversationChange,
4148
4227
  themeMode = "light",
4228
+ showSettings: controlledShowSettings,
4229
+ onSettingsChange,
4149
4230
  className,
4150
4231
  style
4151
4232
  }) {
4152
4233
  const { config } = useCollab();
4153
- const [showSettings, setShowSettings] = React4.useState(false);
4234
+ const isSettingsControlled = controlledShowSettings !== void 0 && onSettingsChange !== void 0;
4235
+ const [internalShowSettings, setInternalShowSettings] = React4.useState(false);
4236
+ const showSettings = isSettingsControlled ? controlledShowSettings : internalShowSettings;
4237
+ const updateShowSettings = isSettingsControlled ? onSettingsChange : setInternalShowSettings;
4154
4238
  const [toast, setToast] = React4.useState(null);
4155
4239
  const messageListRef = React4.useRef(null);
4156
4240
  const {
@@ -4186,6 +4270,7 @@ function CollabPanel({
4186
4270
  config.onOpenDicom(patientData.studyId, patientData.storageId);
4187
4271
  }
4188
4272
  };
4273
+ const handleOpenCase = !hideOpenCase && config.onOpenCase ? () => config.onOpenCase?.(patientData.orderId, patientData.displayOrderId) : void 0;
4189
4274
  const handleJumpToMessage = (messageId) => {
4190
4275
  messageListRef.current?.scrollToMessage(messageId);
4191
4276
  };
@@ -4252,7 +4337,7 @@ function CollabPanel({
4252
4337
  onRemoveUser: channelSettings.removeUser,
4253
4338
  onLeaveChannel: channelSettings.leaveChannel,
4254
4339
  onDeleteChannel: channelSettings.deleteChannel,
4255
- onClose: () => setShowSettings(false)
4340
+ onClose: () => updateShowSettings(false)
4256
4341
  }
4257
4342
  ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4258
4343
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4261,7 +4346,8 @@ function CollabPanel({
4261
4346
  patientData,
4262
4347
  participants,
4263
4348
  onOpenDicom: handleOpenDicom,
4264
- onOpenSettings: () => setShowSettings(true),
4349
+ onOpenCase: handleOpenCase,
4350
+ onOpenSettings: hidePatientName ? void 0 : () => updateShowSettings(true),
4265
4351
  onBack,
4266
4352
  hideName: hidePatientName,
4267
4353
  displayName: conversation?.name
@@ -4404,6 +4490,7 @@ function CollabPopup({
4404
4490
  );
4405
4491
  const [isDragging, setIsDragging] = React4.useState(false);
4406
4492
  const [isMinimized, setIsMinimized] = React4.useState(false);
4493
+ const [showSettings, setShowSettings] = React4.useState(false);
4407
4494
  const [loadedConversation, setLoadedConversation] = React4.useState(null);
4408
4495
  const dragOffset = React4.useRef({ x: 0, y: 0 });
4409
4496
  const containerRef = React4.useRef(null);
@@ -4515,16 +4602,32 @@ function CollabPopup({
4515
4602
  ),
4516
4603
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleCenter, children: [
4517
4604
  /* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, style: styles13.patientName, children: patientData.patientName || titleText }),
4518
- (patientData.labName || patientData.displayOrderId) && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.subRow, children: [
4519
- patientData.labName && /* @__PURE__ */ jsxRuntime.jsx("span", { children: patientData.labName }),
4520
- patientData.labName && patientData.displayOrderId && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.subDot, "aria-hidden": "true", children: "\xB7" }),
4521
- patientData.displayOrderId && /* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles13.mono, children: [
4522
- "#",
4523
- patientData.displayOrderId.slice(-4)
4524
- ] })
4525
- ] })
4605
+ (() => {
4606
+ const parts = [];
4607
+ if (patientData.patientAge) parts.push(String(patientData.patientAge));
4608
+ if (patientData.patientSex) parts.push(String(patientData.patientSex));
4609
+ if (patientData.studyType) parts.push(patientData.studyType);
4610
+ if (patientData.bodyParts && patientData.bodyParts.length > 0) {
4611
+ parts.push(patientData.bodyParts.join(", "));
4612
+ }
4613
+ return parts.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles13.subRow, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.metaText, children: parts.join(" \xB7 ") }) }) : null;
4614
+ })()
4526
4615
  ] }),
4527
4616
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleActions, children: [
4617
+ loadedConversation && /* @__PURE__ */ jsxRuntime.jsxs(
4618
+ "button",
4619
+ {
4620
+ type: "button",
4621
+ onClick: () => setShowSettings(true),
4622
+ style: styles13.titleSettingsPill,
4623
+ "aria-label": `Open channel settings (${loadedConversation.participants.length} participants)`,
4624
+ title: "Channel participants",
4625
+ children: [
4626
+ /* @__PURE__ */ jsxRuntime.jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
4627
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.titleParticipantCount, children: loadedConversation.participants.length })
4628
+ ]
4629
+ }
4630
+ ),
4528
4631
  /* @__PURE__ */ jsxRuntime.jsx(
4529
4632
  "button",
4530
4633
  {
@@ -4559,7 +4662,9 @@ function CollabPopup({
4559
4662
  patientData,
4560
4663
  participantIds,
4561
4664
  hidePatientName: true,
4562
- onConversationChange: setLoadedConversation
4665
+ onConversationChange: setLoadedConversation,
4666
+ showSettings,
4667
+ onSettingsChange: setShowSettings
4563
4668
  }
4564
4669
  ) })
4565
4670
  ]
@@ -4611,19 +4716,17 @@ var styles13 = {
4611
4716
  subRow: {
4612
4717
  display: "flex",
4613
4718
  alignItems: "center",
4614
- gap: SPACE.S2,
4615
4719
  fontSize: FONT_SIZE.sm,
4616
4720
  color: COLOR.neutral500,
4617
4721
  overflow: "hidden",
4618
4722
  textOverflow: "ellipsis",
4619
4723
  whiteSpace: "nowrap"
4620
4724
  },
4621
- subDot: {
4622
- color: COLOR.neutral400
4623
- },
4624
- mono: {
4625
- fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4626
- fontVariantNumeric: "tabular-nums"
4725
+ metaText: {
4726
+ overflow: "hidden",
4727
+ textOverflow: "ellipsis",
4728
+ whiteSpace: "nowrap",
4729
+ minWidth: 0
4627
4730
  },
4628
4731
  titleActions: {
4629
4732
  display: "flex",
@@ -4643,6 +4746,29 @@ var styles13 = {
4643
4746
  color: COLOR.neutral700,
4644
4747
  flexShrink: 0
4645
4748
  },
4749
+ // People-icon + participant-count pill in the title-actions row.
4750
+ // Matches the in-panel settings button's shape (icon + count) so a
4751
+ // user moving between popup mode and standalone mode (CollabInbox
4752
+ // compact) sees the same control language.
4753
+ titleSettingsPill: {
4754
+ height: "36px",
4755
+ display: "inline-flex",
4756
+ alignItems: "center",
4757
+ gap: SPACE.S2,
4758
+ padding: `0 ${SPACE.S3}`,
4759
+ backgroundColor: COLOR.neutral100,
4760
+ border: `1px solid ${COLOR.neutral200}`,
4761
+ borderRadius: RADIUS.md,
4762
+ color: COLOR.neutral600,
4763
+ cursor: "pointer",
4764
+ flexShrink: 0
4765
+ },
4766
+ titleParticipantCount: {
4767
+ fontSize: FONT_SIZE.sm,
4768
+ fontWeight: FONT_WEIGHT.semibold,
4769
+ color: COLOR.neutral700,
4770
+ fontVariantNumeric: "tabular-nums"
4771
+ },
4646
4772
  panelWrapper: {
4647
4773
  flex: 1,
4648
4774
  overflow: "hidden"
@@ -5436,7 +5562,7 @@ function ConversationListItem({
5436
5562
  const studyType = item.patientSnapshot?.studyType;
5437
5563
  const patientId = item.patientSnapshot?.patientId;
5438
5564
  const showStudyRow = !!(studyType || patientId);
5439
- const initials = computeInitials2(item.patientSnapshot?.patientName || displayName);
5565
+ const initials = computeInitials(item.patientSnapshot?.patientName || displayName);
5440
5566
  return /* @__PURE__ */ jsxRuntime.jsxs(
5441
5567
  "button",
5442
5568
  {
@@ -5531,7 +5657,7 @@ function previewText(message) {
5531
5657
  }
5532
5658
  }
5533
5659
  }
5534
- function computeInitials2(name) {
5660
+ function computeInitials(name) {
5535
5661
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("") || "?";
5536
5662
  }
5537
5663
  var styles15 = {