@natoe/colab 0.1.16 → 0.1.17

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 hasActions = hasDicom && onOpenDicom || onOpenCase || onOpenSettings;
1538
+ const actions = hasActions ? /* @__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
+ onOpenSettings && /* @__PURE__ */ jsxs(
1567
+ "button",
1568
+ {
1569
+ onClick: onOpenSettings,
1570
+ style: styles.settingsIconButton,
1571
+ type: "button",
1572
+ "aria-label": `Open channel settings (${participants.length} participants)`,
1573
+ title: "Channel participants",
1574
+ children: [
1575
+ /* @__PURE__ */ jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
1576
+ /* @__PURE__ */ jsx("span", { style: styles.participantCount, children: participants.length })
1577
+ ]
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,46 +1588,14 @@ 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
+ actions
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
- ] })
1574
- ] })
1597
+ !hideName && metaParts.length > 0 && /* @__PURE__ */ jsx("div", { style: styles.metaRow, children: /* @__PURE__ */ jsx("span", { style: styles.metaText, children: metaParts.join(" \xB7 ") }) }),
1598
+ hideName && actions && /* @__PURE__ */ jsx("div", { style: styles.actionsOnlyRow, children: actions })
1575
1599
  ] });
1576
1600
  }
1577
1601
  var styles = {
@@ -1579,16 +1603,16 @@ var styles = {
1579
1603
  display: "flex",
1580
1604
  flexDirection: "column",
1581
1605
  backgroundColor: COLOR.primaryBg,
1582
- borderBottom: `1px solid ${COLOR.neutral200}`
1606
+ borderBottom: `1px solid ${COLOR.neutral200}`,
1607
+ padding: `${SPACE.S2} ${SPACE.S4}`,
1608
+ gap: "2px"
1583
1609
  },
1584
- // Optional name row, only when `hideName` is false (compact-inbox path)
1610
+ // Title row back · name · actions in one horizontal line.
1585
1611
  nameRow: {
1586
1612
  display: "flex",
1587
1613
  alignItems: "center",
1588
1614
  gap: SPACE.S2,
1589
- padding: `${SPACE.S3} ${SPACE.S4} ${SPACE.S2}`,
1590
- borderBottom: `1px solid ${COLOR.neutral200}`,
1591
- backgroundColor: COLOR.white
1615
+ minWidth: 0
1592
1616
  },
1593
1617
  backButton: {
1594
1618
  width: SIZE.controlIcon,
@@ -1604,57 +1628,45 @@ var styles = {
1604
1628
  flexShrink: 0
1605
1629
  },
1606
1630
  nameText: {
1631
+ flex: 1,
1632
+ minWidth: 0,
1607
1633
  fontSize: FONT_SIZE.lg,
1608
1634
  fontWeight: FONT_WEIGHT.bold,
1609
1635
  color: COLOR.neutral900,
1610
1636
  lineHeight: LINE_HEIGHT.tight,
1611
1637
  letterSpacing: "-0.01em",
1612
- minWidth: 0,
1613
1638
  overflow: "hidden",
1614
1639
  textOverflow: "ellipsis",
1615
1640
  whiteSpace: "nowrap"
1616
1641
  },
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.
1642
+ // Sub-row beneath the title small muted meta. Aligned under the
1643
+ // name (past the back button) so it visually anchors to the title
1644
+ // rather than the panel edge.
1629
1645
  metaRow: {
1630
1646
  display: "flex",
1631
- flexWrap: "wrap",
1632
- columnGap: SPACE.S5,
1633
- rowGap: SPACE.S2,
1647
+ alignItems: "center",
1648
+ paddingLeft: `calc(${SIZE.controlIcon} + ${SPACE.S2} - ${SPACE.S2})`,
1634
1649
  minWidth: 0
1635
1650
  },
1636
- metaCol: {
1651
+ // When hideName=true (popup mode), only the actions render. The
1652
+ // popup's own title bar carries the name + meta above this row, so
1653
+ // the actions land flush-left here — pinning them to the right
1654
+ // edge would leave the whole left half of the strip awkwardly
1655
+ // empty (the meta that used to fill it now lives in the title bar).
1656
+ actionsOnlyRow: {
1637
1657
  display: "flex",
1638
- flexDirection: "column",
1639
- gap: "2px",
1658
+ justifyContent: "flex-start",
1640
1659
  minWidth: 0
1641
1660
  },
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: {
1661
+ metaText: {
1651
1662
  fontSize: FONT_SIZE.sm,
1652
- fontWeight: FONT_WEIGHT.semibold,
1653
- color: COLOR.neutral900,
1654
- whiteSpace: "nowrap",
1663
+ fontWeight: FONT_WEIGHT.medium,
1664
+ color: COLOR.neutral600,
1665
+ lineHeight: LINE_HEIGHT.tight,
1655
1666
  overflow: "hidden",
1656
1667
  textOverflow: "ellipsis",
1657
- maxWidth: "180px"
1668
+ whiteSpace: "nowrap",
1669
+ minWidth: 0
1658
1670
  },
1659
1671
  actions: {
1660
1672
  display: "flex",
@@ -1666,7 +1678,24 @@ var styles = {
1666
1678
  alignItems: "center",
1667
1679
  gap: SPACE.S2,
1668
1680
  minHeight: SIZE.control,
1669
- padding: `${SPACE.S2} 14px`,
1681
+ padding: `${SPACE.S1} ${SPACE.S3}`,
1682
+ fontSize: FONT_SIZE.sm,
1683
+ fontWeight: FONT_WEIGHT.semibold,
1684
+ color: COLOR.primary,
1685
+ backgroundColor: COLOR.white,
1686
+ border: `1.5px solid ${COLOR.primary}`,
1687
+ borderRadius: RADIUS.lg,
1688
+ cursor: "pointer"
1689
+ },
1690
+ // Sibling of the DICOM button — same shape so the two read as a pair.
1691
+ // Click hands off to the host's `onOpenCase` callback (role-aware
1692
+ // routing lives on the host side).
1693
+ openCaseButton: {
1694
+ display: "inline-flex",
1695
+ alignItems: "center",
1696
+ gap: SPACE.S2,
1697
+ minHeight: SIZE.control,
1698
+ padding: `${SPACE.S1} ${SPACE.S3}`,
1670
1699
  fontSize: FONT_SIZE.sm,
1671
1700
  fontWeight: FONT_WEIGHT.semibold,
1672
1701
  color: COLOR.primary,
@@ -1675,10 +1704,8 @@ var styles = {
1675
1704
  borderRadius: RADIUS.lg,
1676
1705
  cursor: "pointer"
1677
1706
  },
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).
1707
+ // Channel-info button: people icon + participant count. Opens the
1708
+ // channel settings overlay.
1682
1709
  settingsIconButton: {
1683
1710
  minHeight: SIZE.control,
1684
1711
  display: "inline-flex",
@@ -2158,6 +2185,62 @@ function AlertIcon({ size = 14, color = "currentColor" }) {
2158
2185
  }
2159
2186
  );
2160
2187
  }
2188
+ function RoleAvatarIcon({
2189
+ role,
2190
+ size = 18,
2191
+ color = "currentColor",
2192
+ className
2193
+ }) {
2194
+ const common = {
2195
+ width: size,
2196
+ height: size,
2197
+ viewBox: "0 0 24 24",
2198
+ fill: "none",
2199
+ stroke: color,
2200
+ strokeWidth: 1.9,
2201
+ strokeLinecap: "round",
2202
+ strokeLinejoin: "round",
2203
+ "aria-hidden": true,
2204
+ className
2205
+ };
2206
+ switch (role) {
2207
+ case "radiologist":
2208
+ return /* @__PURE__ */ jsxs("svg", { ...common, children: [
2209
+ /* @__PURE__ */ jsx("path", { d: "M7.5 8.5h9" }),
2210
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "9.5", r: "4" }),
2211
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "7.5", r: "1", fill: color, stroke: "none" }),
2212
+ /* @__PURE__ */ jsx("path", { d: "M4 21v-1.5a5 5 0 0 1 5-5h6a5 5 0 0 1 5 5V21" })
2213
+ ] });
2214
+ case "lab":
2215
+ return /* @__PURE__ */ jsxs("svg", { ...common, children: [
2216
+ /* @__PURE__ */ jsx("path", { d: "M3 21V9l9-6 9 6v12" }),
2217
+ /* @__PURE__ */ jsx("path", { d: "M12 11v6" }),
2218
+ /* @__PURE__ */ jsx("path", { d: "M9 14h6" }),
2219
+ /* @__PURE__ */ jsx("path", { d: "M11 21v-3h2v3" }),
2220
+ /* @__PURE__ */ jsx("path", { d: "M3 21h18" })
2221
+ ] });
2222
+ case "physician":
2223
+ return /* @__PURE__ */ jsxs("svg", { ...common, children: [
2224
+ /* @__PURE__ */ jsx("path", { d: "M7 3v5a4 4 0 0 0 8 0V3" }),
2225
+ /* @__PURE__ */ jsx("path", { d: "M5 3h2" }),
2226
+ /* @__PURE__ */ jsx("path", { d: "M15 3h2" }),
2227
+ /* @__PURE__ */ jsx("path", { d: "M11 12v3a5 5 0 0 0 5 5h0a5 5 0 0 0 5-5v-2" }),
2228
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "11", r: "2" })
2229
+ ] });
2230
+ case "admin":
2231
+ return /* @__PURE__ */ jsxs("svg", { ...common, children: [
2232
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" }),
2233
+ /* @__PURE__ */ jsx("path", { d: "M12 2v3" }),
2234
+ /* @__PURE__ */ jsx("path", { d: "M12 19v3" }),
2235
+ /* @__PURE__ */ jsx("path", { d: "M5 12H2" }),
2236
+ /* @__PURE__ */ jsx("path", { d: "M22 12h-3" }),
2237
+ /* @__PURE__ */ jsx("path", { d: "M5.6 5.6l2 2" }),
2238
+ /* @__PURE__ */ jsx("path", { d: "M16.4 16.4l2 2" }),
2239
+ /* @__PURE__ */ jsx("path", { d: "M5.6 18.4l2-2" }),
2240
+ /* @__PURE__ */ jsx("path", { d: "M16.4 7.6l2-2" })
2241
+ ] });
2242
+ }
2243
+ }
2161
2244
  function Spinner({ size = 16, color = COLOR.neutral500, thickness = 2 }) {
2162
2245
  return /* @__PURE__ */ jsx(
2163
2246
  "span",
@@ -2246,12 +2329,11 @@ function MessageBubble({
2246
2329
  ...styles5.authorAvatar,
2247
2330
  backgroundColor: roleColor(message.senderRole)
2248
2331
  },
2249
- "aria-hidden": "true",
2250
- children: computeInitials(message.senderName)
2332
+ "aria-label": ROLE_LABELS[message.senderRole] ?? message.senderRole,
2333
+ children: /* @__PURE__ */ jsx(RoleAvatarIcon, { role: message.senderRole, size: 20, color: COLOR.white })
2251
2334
  }
2252
2335
  ),
2253
- /* @__PURE__ */ jsx("span", { style: styles5.authorName, children: message.senderName }),
2254
- /* @__PURE__ */ jsx(RoleBadge, { role: message.senderRole })
2336
+ /* @__PURE__ */ jsx("span", { style: styles5.authorName, children: message.senderName })
2255
2337
  ] }),
2256
2338
  /* @__PURE__ */ jsxs(
2257
2339
  "div",
@@ -2326,9 +2408,6 @@ function MessageBubble({
2326
2408
  }
2327
2409
  );
2328
2410
  }
2329
- function computeInitials(name) {
2330
- return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
2331
- }
2332
2411
  function roleColor(role) {
2333
2412
  switch (role) {
2334
2413
  case "radiologist":
@@ -2413,9 +2492,6 @@ function SystemBubble({ message }) {
2413
2492
  /* @__PURE__ */ jsx("span", { style: styles5.systemTime, children: formatTime(message.insertedAt) })
2414
2493
  ] }) });
2415
2494
  }
2416
- function RoleBadge({ role }) {
2417
- return /* @__PURE__ */ jsx("span", { style: { ...styles5.roleBadge, backgroundColor: roleColor(role) }, children: ROLE_LABELS[role] ?? role });
2418
- }
2419
2495
  function formatTime(iso) {
2420
2496
  try {
2421
2497
  return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
@@ -2517,16 +2593,6 @@ var styles5 = {
2517
2593
  color: "#fff4d6",
2518
2594
  backgroundColor: "rgba(255, 255, 255, 0.2)"
2519
2595
  },
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
2596
  textBody: {
2531
2597
  margin: 0,
2532
2598
  fontSize: FONT_SIZE.md,
@@ -2990,9 +3056,6 @@ var styles6 = {
2990
3056
  alignItems: "center",
2991
3057
  gap: SPACE.S3,
2992
3058
  margin: `${SPACE.S4} 0 ${SPACE.S3}`,
2993
- position: "sticky",
2994
- top: SPACE.S1,
2995
- zIndex: Z_INDEX.sticky,
2996
3059
  pointerEvents: "none"
2997
3060
  },
2998
3061
  dayHr: {
@@ -3007,11 +3070,7 @@ var styles6 = {
3007
3070
  color: COLOR.neutral500,
3008
3071
  textTransform: "uppercase",
3009
3072
  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
3073
  padding: `2px ${SPACE.S2}`,
3014
- backgroundColor: COLOR.white,
3015
3074
  borderRadius: RADIUS.sm
3016
3075
  },
3017
3076
  bubbleSlot: {
@@ -3309,7 +3368,7 @@ function MessageInput({
3309
3368
  style: styles8.textarea
3310
3369
  }
3311
3370
  ),
3312
- /* @__PURE__ */ jsxs(
3371
+ /* @__PURE__ */ jsx(
3313
3372
  "button",
3314
3373
  {
3315
3374
  onClick: handleSendText,
@@ -3321,10 +3380,7 @@ function MessageInput({
3321
3380
  opacity: text.trim() ? 1 : 0.5
3322
3381
  },
3323
3382
  type: "button",
3324
- children: [
3325
- /* @__PURE__ */ jsx(SendIcon, { size: 20, color: COLOR.white }),
3326
- /* @__PURE__ */ jsx("span", { style: styles8.sendLabel, children: "Send" })
3327
- ]
3383
+ children: /* @__PURE__ */ jsx(SendIcon, { size: 20, color: COLOR.white })
3328
3384
  }
3329
3385
  )
3330
3386
  ] })
@@ -3422,29 +3478,33 @@ var styles8 = {
3422
3478
  fontFamily: "inherit",
3423
3479
  transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease"
3424
3480
  },
3425
- // Pill-shaped send: icon + "Send" label. Always labelled (never icon-
3426
- // only) so the affordance is obvious to first-time users.
3481
+ // Square icon-only send button. The arrow inside an obvious input
3482
+ // context is affordance enough; the word "Send" used to sit next to
3483
+ // it but added clutter without disambiguating anything.
3427
3484
  sendButton: {
3428
3485
  display: "inline-flex",
3429
3486
  alignItems: "center",
3430
- gap: "6px",
3431
- minWidth: "48px",
3487
+ justifyContent: "center",
3488
+ width: "48px",
3432
3489
  height: "48px",
3433
- padding: `0 ${SPACE.S4}`,
3434
3490
  backgroundColor: COLOR.primary,
3435
3491
  color: COLOR.white,
3436
3492
  border: "none",
3437
3493
  borderRadius: "14px",
3438
3494
  cursor: "pointer",
3439
3495
  flexShrink: 0,
3440
- fontFamily: "inherit",
3441
- fontSize: "15px",
3442
- fontWeight: FONT_WEIGHT.semibold,
3443
3496
  transition: "opacity 120ms ease, background-color 120ms ease"
3444
- },
3445
- sendLabel: {
3446
- lineHeight: 1
3447
3497
  }};
3498
+ var ROLE_AVATAR_BG = {
3499
+ radiologist: "#4f46e5",
3500
+ // indigo
3501
+ lab: "#2563eb",
3502
+ // brand blue
3503
+ physician: "#0284c7",
3504
+ // sky
3505
+ admin: "#64748b"
3506
+ // slate
3507
+ };
3448
3508
  function ParticipantsList({
3449
3509
  participants,
3450
3510
  currentUserId,
@@ -3454,7 +3514,17 @@ function ParticipantsList({
3454
3514
  }) {
3455
3515
  return /* @__PURE__ */ jsx("div", { className, style: styles9.list, children: participants.map((participant) => /* @__PURE__ */ jsxs("div", { style: styles9.item, children: [
3456
3516
  /* @__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() }),
3517
+ participant.avatar ? /* @__PURE__ */ jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsx(
3518
+ "span",
3519
+ {
3520
+ style: {
3521
+ ...styles9.avatarFallback,
3522
+ backgroundColor: ROLE_AVATAR_BG[participant.userRole]
3523
+ },
3524
+ "aria-hidden": "true",
3525
+ children: /* @__PURE__ */ jsx(RoleAvatarIcon, { role: participant.userRole, size: 18, color: COLOR.white })
3526
+ }
3527
+ ),
3458
3528
  /* @__PURE__ */ jsx(
3459
3529
  "span",
3460
3530
  {
@@ -3470,7 +3540,7 @@ function ParticipantsList({
3470
3540
  participant.userName ?? fallbackName(participant.userRole),
3471
3541
  participant.userId === currentUserId && /* @__PURE__ */ jsx("span", { style: styles9.youBadge, children: " (you)" })
3472
3542
  ] }),
3473
- /* @__PURE__ */ jsx(RoleBadge2, { role: participant.userRole })
3543
+ /* @__PURE__ */ jsx(RoleBadge, { role: participant.userRole })
3474
3544
  ] }),
3475
3545
  isAdmin && participant.userId !== currentUserId && /* @__PURE__ */ jsx(
3476
3546
  "button",
@@ -3498,7 +3568,7 @@ function fallbackName(role) {
3498
3568
  return "User";
3499
3569
  }
3500
3570
  }
3501
- function RoleBadge2({ role }) {
3571
+ function RoleBadge({ role }) {
3502
3572
  const colors = {
3503
3573
  radiologist: { bg: "#eef2ff", text: "#4f46e5" },
3504
3574
  // indigo
@@ -3540,13 +3610,10 @@ var styles9 = {
3540
3610
  width: "32px",
3541
3611
  height: "32px",
3542
3612
  borderRadius: RADIUS.full,
3543
- backgroundColor: COLOR.neutral200,
3544
3613
  display: "flex",
3545
3614
  alignItems: "center",
3546
3615
  justifyContent: "center",
3547
- fontSize: FONT_SIZE.sm,
3548
- fontWeight: FONT_WEIGHT.semibold,
3549
- color: COLOR.neutral500
3616
+ color: COLOR.white
3550
3617
  },
3551
3618
  statusDot: {
3552
3619
  position: "absolute",
@@ -4138,6 +4205,7 @@ function CollabPanel({
4138
4205
  showSeenBy = true,
4139
4206
  onBack,
4140
4207
  hidePatientName = false,
4208
+ hideOpenCase = false,
4141
4209
  onConversationChange,
4142
4210
  themeMode = "light",
4143
4211
  className,
@@ -4180,6 +4248,7 @@ function CollabPanel({
4180
4248
  config.onOpenDicom(patientData.studyId, patientData.storageId);
4181
4249
  }
4182
4250
  };
4251
+ const handleOpenCase = !hideOpenCase && config.onOpenCase ? () => config.onOpenCase?.(patientData.orderId, patientData.displayOrderId) : void 0;
4183
4252
  const handleJumpToMessage = (messageId) => {
4184
4253
  messageListRef.current?.scrollToMessage(messageId);
4185
4254
  };
@@ -4255,6 +4324,7 @@ function CollabPanel({
4255
4324
  patientData,
4256
4325
  participants,
4257
4326
  onOpenDicom: handleOpenDicom,
4327
+ onOpenCase: handleOpenCase,
4258
4328
  onOpenSettings: () => setShowSettings(true),
4259
4329
  onBack,
4260
4330
  hideName: hidePatientName,
@@ -4509,14 +4579,16 @@ function CollabPopup({
4509
4579
  ),
4510
4580
  /* @__PURE__ */ jsxs("div", { style: styles13.titleCenter, children: [
4511
4581
  /* @__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
- ] })
4582
+ (() => {
4583
+ const parts = [];
4584
+ if (patientData.patientAge) parts.push(String(patientData.patientAge));
4585
+ if (patientData.patientSex) parts.push(String(patientData.patientSex));
4586
+ if (patientData.studyType) parts.push(patientData.studyType);
4587
+ if (patientData.bodyParts && patientData.bodyParts.length > 0) {
4588
+ parts.push(patientData.bodyParts.join(", "));
4589
+ }
4590
+ return parts.length > 0 ? /* @__PURE__ */ jsx("div", { style: styles13.subRow, children: /* @__PURE__ */ jsx("span", { style: styles13.metaText, children: parts.join(" \xB7 ") }) }) : null;
4591
+ })()
4520
4592
  ] }),
4521
4593
  /* @__PURE__ */ jsxs("div", { style: styles13.titleActions, children: [
4522
4594
  /* @__PURE__ */ jsx(
@@ -4605,19 +4677,17 @@ var styles13 = {
4605
4677
  subRow: {
4606
4678
  display: "flex",
4607
4679
  alignItems: "center",
4608
- gap: SPACE.S2,
4609
4680
  fontSize: FONT_SIZE.sm,
4610
4681
  color: COLOR.neutral500,
4611
4682
  overflow: "hidden",
4612
4683
  textOverflow: "ellipsis",
4613
4684
  whiteSpace: "nowrap"
4614
4685
  },
4615
- subDot: {
4616
- color: COLOR.neutral400
4617
- },
4618
- mono: {
4619
- fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4620
- fontVariantNumeric: "tabular-nums"
4686
+ metaText: {
4687
+ overflow: "hidden",
4688
+ textOverflow: "ellipsis",
4689
+ whiteSpace: "nowrap",
4690
+ minWidth: 0
4621
4691
  },
4622
4692
  titleActions: {
4623
4693
  display: "flex",
@@ -5430,7 +5500,7 @@ function ConversationListItem({
5430
5500
  const studyType = item.patientSnapshot?.studyType;
5431
5501
  const patientId = item.patientSnapshot?.patientId;
5432
5502
  const showStudyRow = !!(studyType || patientId);
5433
- const initials = computeInitials2(item.patientSnapshot?.patientName || displayName);
5503
+ const initials = computeInitials(item.patientSnapshot?.patientName || displayName);
5434
5504
  return /* @__PURE__ */ jsxs(
5435
5505
  "button",
5436
5506
  {
@@ -5525,7 +5595,7 @@ function previewText(message) {
5525
5595
  }
5526
5596
  }
5527
5597
  }
5528
- function computeInitials2(name) {
5598
+ function computeInitials(name) {
5529
5599
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("") || "?";
5530
5600
  }
5531
5601
  var styles15 = {