@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.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 hasActions = hasDicom && onOpenDicom || onOpenCase || onOpenSettings;
1544
+ const actions = hasActions ? /* @__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
+ onOpenSettings && /* @__PURE__ */ jsxRuntime.jsxs(
1573
+ "button",
1574
+ {
1575
+ onClick: onOpenSettings,
1576
+ style: styles.settingsIconButton,
1577
+ type: "button",
1578
+ "aria-label": `Open channel settings (${participants.length} participants)`,
1579
+ title: "Channel participants",
1580
+ children: [
1581
+ /* @__PURE__ */ jsxRuntime.jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
1582
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.participantCount, children: participants.length })
1583
+ ]
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,46 +1594,14 @@ 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
+ actions
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
- ] })
1580
- ] })
1603
+ !hideName && metaParts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.metaRow, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.metaText, children: metaParts.join(" \xB7 ") }) }),
1604
+ hideName && actions && /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.actionsOnlyRow, children: actions })
1581
1605
  ] });
1582
1606
  }
1583
1607
  var styles = {
@@ -1585,16 +1609,16 @@ var styles = {
1585
1609
  display: "flex",
1586
1610
  flexDirection: "column",
1587
1611
  backgroundColor: COLOR.primaryBg,
1588
- borderBottom: `1px solid ${COLOR.neutral200}`
1612
+ borderBottom: `1px solid ${COLOR.neutral200}`,
1613
+ padding: `${SPACE.S2} ${SPACE.S4}`,
1614
+ gap: "2px"
1589
1615
  },
1590
- // Optional name row, only when `hideName` is false (compact-inbox path)
1616
+ // Title row back · name · actions in one horizontal line.
1591
1617
  nameRow: {
1592
1618
  display: "flex",
1593
1619
  alignItems: "center",
1594
1620
  gap: SPACE.S2,
1595
- padding: `${SPACE.S3} ${SPACE.S4} ${SPACE.S2}`,
1596
- borderBottom: `1px solid ${COLOR.neutral200}`,
1597
- backgroundColor: COLOR.white
1621
+ minWidth: 0
1598
1622
  },
1599
1623
  backButton: {
1600
1624
  width: SIZE.controlIcon,
@@ -1610,57 +1634,45 @@ var styles = {
1610
1634
  flexShrink: 0
1611
1635
  },
1612
1636
  nameText: {
1637
+ flex: 1,
1638
+ minWidth: 0,
1613
1639
  fontSize: FONT_SIZE.lg,
1614
1640
  fontWeight: FONT_WEIGHT.bold,
1615
1641
  color: COLOR.neutral900,
1616
1642
  lineHeight: LINE_HEIGHT.tight,
1617
1643
  letterSpacing: "-0.01em",
1618
- minWidth: 0,
1619
1644
  overflow: "hidden",
1620
1645
  textOverflow: "ellipsis",
1621
1646
  whiteSpace: "nowrap"
1622
1647
  },
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.
1648
+ // Sub-row beneath the title small muted meta. Aligned under the
1649
+ // name (past the back button) so it visually anchors to the title
1650
+ // rather than the panel edge.
1635
1651
  metaRow: {
1636
1652
  display: "flex",
1637
- flexWrap: "wrap",
1638
- columnGap: SPACE.S5,
1639
- rowGap: SPACE.S2,
1653
+ alignItems: "center",
1654
+ paddingLeft: `calc(${SIZE.controlIcon} + ${SPACE.S2} - ${SPACE.S2})`,
1640
1655
  minWidth: 0
1641
1656
  },
1642
- metaCol: {
1657
+ // When hideName=true (popup mode), only the actions render. The
1658
+ // popup's own title bar carries the name + meta above this row, so
1659
+ // the actions land flush-left here — pinning them to the right
1660
+ // edge would leave the whole left half of the strip awkwardly
1661
+ // empty (the meta that used to fill it now lives in the title bar).
1662
+ actionsOnlyRow: {
1643
1663
  display: "flex",
1644
- flexDirection: "column",
1645
- gap: "2px",
1664
+ justifyContent: "flex-start",
1646
1665
  minWidth: 0
1647
1666
  },
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: {
1667
+ metaText: {
1657
1668
  fontSize: FONT_SIZE.sm,
1658
- fontWeight: FONT_WEIGHT.semibold,
1659
- color: COLOR.neutral900,
1660
- whiteSpace: "nowrap",
1669
+ fontWeight: FONT_WEIGHT.medium,
1670
+ color: COLOR.neutral600,
1671
+ lineHeight: LINE_HEIGHT.tight,
1661
1672
  overflow: "hidden",
1662
1673
  textOverflow: "ellipsis",
1663
- maxWidth: "180px"
1674
+ whiteSpace: "nowrap",
1675
+ minWidth: 0
1664
1676
  },
1665
1677
  actions: {
1666
1678
  display: "flex",
@@ -1672,7 +1684,24 @@ var styles = {
1672
1684
  alignItems: "center",
1673
1685
  gap: SPACE.S2,
1674
1686
  minHeight: SIZE.control,
1675
- padding: `${SPACE.S2} 14px`,
1687
+ padding: `${SPACE.S1} ${SPACE.S3}`,
1688
+ fontSize: FONT_SIZE.sm,
1689
+ fontWeight: FONT_WEIGHT.semibold,
1690
+ color: COLOR.primary,
1691
+ backgroundColor: COLOR.white,
1692
+ border: `1.5px solid ${COLOR.primary}`,
1693
+ borderRadius: RADIUS.lg,
1694
+ cursor: "pointer"
1695
+ },
1696
+ // Sibling of the DICOM button — same shape so the two read as a pair.
1697
+ // Click hands off to the host's `onOpenCase` callback (role-aware
1698
+ // routing lives on the host side).
1699
+ openCaseButton: {
1700
+ display: "inline-flex",
1701
+ alignItems: "center",
1702
+ gap: SPACE.S2,
1703
+ minHeight: SIZE.control,
1704
+ padding: `${SPACE.S1} ${SPACE.S3}`,
1676
1705
  fontSize: FONT_SIZE.sm,
1677
1706
  fontWeight: FONT_WEIGHT.semibold,
1678
1707
  color: COLOR.primary,
@@ -1681,10 +1710,8 @@ var styles = {
1681
1710
  borderRadius: RADIUS.lg,
1682
1711
  cursor: "pointer"
1683
1712
  },
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).
1713
+ // Channel-info button: people icon + participant count. Opens the
1714
+ // channel settings overlay.
1688
1715
  settingsIconButton: {
1689
1716
  minHeight: SIZE.control,
1690
1717
  display: "inline-flex",
@@ -2164,6 +2191,62 @@ function AlertIcon({ size = 14, color = "currentColor" }) {
2164
2191
  }
2165
2192
  );
2166
2193
  }
2194
+ function RoleAvatarIcon({
2195
+ role,
2196
+ size = 18,
2197
+ color = "currentColor",
2198
+ className
2199
+ }) {
2200
+ const common = {
2201
+ width: size,
2202
+ height: size,
2203
+ viewBox: "0 0 24 24",
2204
+ fill: "none",
2205
+ stroke: color,
2206
+ strokeWidth: 1.9,
2207
+ strokeLinecap: "round",
2208
+ strokeLinejoin: "round",
2209
+ "aria-hidden": true,
2210
+ className
2211
+ };
2212
+ switch (role) {
2213
+ case "radiologist":
2214
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...common, children: [
2215
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7.5 8.5h9" }),
2216
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "9.5", r: "4" }),
2217
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "7.5", r: "1", fill: color, stroke: "none" }),
2218
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 21v-1.5a5 5 0 0 1 5-5h6a5 5 0 0 1 5 5V21" })
2219
+ ] });
2220
+ case "lab":
2221
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...common, children: [
2222
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21V9l9-6 9 6v12" }),
2223
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 11v6" }),
2224
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 14h6" }),
2225
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M11 21v-3h2v3" }),
2226
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21h18" })
2227
+ ] });
2228
+ case "physician":
2229
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...common, children: [
2230
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 3v5a4 4 0 0 0 8 0V3" }),
2231
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 3h2" }),
2232
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 3h2" }),
2233
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M11 12v3a5 5 0 0 0 5 5h0a5 5 0 0 0 5-5v-2" }),
2234
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "21", cy: "11", r: "2" })
2235
+ ] });
2236
+ case "admin":
2237
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { ...common, children: [
2238
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3" }),
2239
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2v3" }),
2240
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19v3" }),
2241
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 12H2" }),
2242
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M22 12h-3" }),
2243
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5.6 5.6l2 2" }),
2244
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.4 16.4l2 2" }),
2245
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5.6 18.4l2-2" }),
2246
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16.4 7.6l2-2" })
2247
+ ] });
2248
+ }
2249
+ }
2167
2250
  function Spinner({ size = 16, color = COLOR.neutral500, thickness = 2 }) {
2168
2251
  return /* @__PURE__ */ jsxRuntime.jsx(
2169
2252
  "span",
@@ -2252,12 +2335,11 @@ function MessageBubble({
2252
2335
  ...styles5.authorAvatar,
2253
2336
  backgroundColor: roleColor(message.senderRole)
2254
2337
  },
2255
- "aria-hidden": "true",
2256
- children: computeInitials(message.senderName)
2338
+ "aria-label": ROLE_LABELS[message.senderRole] ?? message.senderRole,
2339
+ children: /* @__PURE__ */ jsxRuntime.jsx(RoleAvatarIcon, { role: message.senderRole, size: 20, color: COLOR.white })
2257
2340
  }
2258
2341
  ),
2259
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.authorName, children: message.senderName }),
2260
- /* @__PURE__ */ jsxRuntime.jsx(RoleBadge, { role: message.senderRole })
2342
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.authorName, children: message.senderName })
2261
2343
  ] }),
2262
2344
  /* @__PURE__ */ jsxRuntime.jsxs(
2263
2345
  "div",
@@ -2332,9 +2414,6 @@ function MessageBubble({
2332
2414
  }
2333
2415
  );
2334
2416
  }
2335
- function computeInitials(name) {
2336
- return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
2337
- }
2338
2417
  function roleColor(role) {
2339
2418
  switch (role) {
2340
2419
  case "radiologist":
@@ -2419,9 +2498,6 @@ function SystemBubble({ message }) {
2419
2498
  /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.systemTime, children: formatTime(message.insertedAt) })
2420
2499
  ] }) });
2421
2500
  }
2422
- function RoleBadge({ role }) {
2423
- return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles5.roleBadge, backgroundColor: roleColor(role) }, children: ROLE_LABELS[role] ?? role });
2424
- }
2425
2501
  function formatTime(iso) {
2426
2502
  try {
2427
2503
  return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
@@ -2523,16 +2599,6 @@ var styles5 = {
2523
2599
  color: "#fff4d6",
2524
2600
  backgroundColor: "rgba(255, 255, 255, 0.2)"
2525
2601
  },
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
2602
  textBody: {
2537
2603
  margin: 0,
2538
2604
  fontSize: FONT_SIZE.md,
@@ -2996,9 +3062,6 @@ var styles6 = {
2996
3062
  alignItems: "center",
2997
3063
  gap: SPACE.S3,
2998
3064
  margin: `${SPACE.S4} 0 ${SPACE.S3}`,
2999
- position: "sticky",
3000
- top: SPACE.S1,
3001
- zIndex: Z_INDEX.sticky,
3002
3065
  pointerEvents: "none"
3003
3066
  },
3004
3067
  dayHr: {
@@ -3013,11 +3076,7 @@ var styles6 = {
3013
3076
  color: COLOR.neutral500,
3014
3077
  textTransform: "uppercase",
3015
3078
  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
3079
  padding: `2px ${SPACE.S2}`,
3020
- backgroundColor: COLOR.white,
3021
3080
  borderRadius: RADIUS.sm
3022
3081
  },
3023
3082
  bubbleSlot: {
@@ -3315,7 +3374,7 @@ function MessageInput({
3315
3374
  style: styles8.textarea
3316
3375
  }
3317
3376
  ),
3318
- /* @__PURE__ */ jsxRuntime.jsxs(
3377
+ /* @__PURE__ */ jsxRuntime.jsx(
3319
3378
  "button",
3320
3379
  {
3321
3380
  onClick: handleSendText,
@@ -3327,10 +3386,7 @@ function MessageInput({
3327
3386
  opacity: text.trim() ? 1 : 0.5
3328
3387
  },
3329
3388
  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
- ]
3389
+ children: /* @__PURE__ */ jsxRuntime.jsx(SendIcon, { size: 20, color: COLOR.white })
3334
3390
  }
3335
3391
  )
3336
3392
  ] })
@@ -3428,29 +3484,33 @@ var styles8 = {
3428
3484
  fontFamily: "inherit",
3429
3485
  transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease"
3430
3486
  },
3431
- // Pill-shaped send: icon + "Send" label. Always labelled (never icon-
3432
- // only) so the affordance is obvious to first-time users.
3487
+ // Square icon-only send button. The arrow inside an obvious input
3488
+ // context is affordance enough; the word "Send" used to sit next to
3489
+ // it but added clutter without disambiguating anything.
3433
3490
  sendButton: {
3434
3491
  display: "inline-flex",
3435
3492
  alignItems: "center",
3436
- gap: "6px",
3437
- minWidth: "48px",
3493
+ justifyContent: "center",
3494
+ width: "48px",
3438
3495
  height: "48px",
3439
- padding: `0 ${SPACE.S4}`,
3440
3496
  backgroundColor: COLOR.primary,
3441
3497
  color: COLOR.white,
3442
3498
  border: "none",
3443
3499
  borderRadius: "14px",
3444
3500
  cursor: "pointer",
3445
3501
  flexShrink: 0,
3446
- fontFamily: "inherit",
3447
- fontSize: "15px",
3448
- fontWeight: FONT_WEIGHT.semibold,
3449
3502
  transition: "opacity 120ms ease, background-color 120ms ease"
3450
- },
3451
- sendLabel: {
3452
- lineHeight: 1
3453
3503
  }};
3504
+ var ROLE_AVATAR_BG = {
3505
+ radiologist: "#4f46e5",
3506
+ // indigo
3507
+ lab: "#2563eb",
3508
+ // brand blue
3509
+ physician: "#0284c7",
3510
+ // sky
3511
+ admin: "#64748b"
3512
+ // slate
3513
+ };
3454
3514
  function ParticipantsList({
3455
3515
  participants,
3456
3516
  currentUserId,
@@ -3460,7 +3520,17 @@ function ParticipantsList({
3460
3520
  }) {
3461
3521
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: styles9.list, children: participants.map((participant) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles9.item, children: [
3462
3522
  /* @__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() }),
3523
+ participant.avatar ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsxRuntime.jsx(
3524
+ "span",
3525
+ {
3526
+ style: {
3527
+ ...styles9.avatarFallback,
3528
+ backgroundColor: ROLE_AVATAR_BG[participant.userRole]
3529
+ },
3530
+ "aria-hidden": "true",
3531
+ children: /* @__PURE__ */ jsxRuntime.jsx(RoleAvatarIcon, { role: participant.userRole, size: 18, color: COLOR.white })
3532
+ }
3533
+ ),
3464
3534
  /* @__PURE__ */ jsxRuntime.jsx(
3465
3535
  "span",
3466
3536
  {
@@ -3476,7 +3546,7 @@ function ParticipantsList({
3476
3546
  participant.userName ?? fallbackName(participant.userRole),
3477
3547
  participant.userId === currentUserId && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles9.youBadge, children: " (you)" })
3478
3548
  ] }),
3479
- /* @__PURE__ */ jsxRuntime.jsx(RoleBadge2, { role: participant.userRole })
3549
+ /* @__PURE__ */ jsxRuntime.jsx(RoleBadge, { role: participant.userRole })
3480
3550
  ] }),
3481
3551
  isAdmin && participant.userId !== currentUserId && /* @__PURE__ */ jsxRuntime.jsx(
3482
3552
  "button",
@@ -3504,7 +3574,7 @@ function fallbackName(role) {
3504
3574
  return "User";
3505
3575
  }
3506
3576
  }
3507
- function RoleBadge2({ role }) {
3577
+ function RoleBadge({ role }) {
3508
3578
  const colors = {
3509
3579
  radiologist: { bg: "#eef2ff", text: "#4f46e5" },
3510
3580
  // indigo
@@ -3546,13 +3616,10 @@ var styles9 = {
3546
3616
  width: "32px",
3547
3617
  height: "32px",
3548
3618
  borderRadius: RADIUS.full,
3549
- backgroundColor: COLOR.neutral200,
3550
3619
  display: "flex",
3551
3620
  alignItems: "center",
3552
3621
  justifyContent: "center",
3553
- fontSize: FONT_SIZE.sm,
3554
- fontWeight: FONT_WEIGHT.semibold,
3555
- color: COLOR.neutral500
3622
+ color: COLOR.white
3556
3623
  },
3557
3624
  statusDot: {
3558
3625
  position: "absolute",
@@ -4144,6 +4211,7 @@ function CollabPanel({
4144
4211
  showSeenBy = true,
4145
4212
  onBack,
4146
4213
  hidePatientName = false,
4214
+ hideOpenCase = false,
4147
4215
  onConversationChange,
4148
4216
  themeMode = "light",
4149
4217
  className,
@@ -4186,6 +4254,7 @@ function CollabPanel({
4186
4254
  config.onOpenDicom(patientData.studyId, patientData.storageId);
4187
4255
  }
4188
4256
  };
4257
+ const handleOpenCase = !hideOpenCase && config.onOpenCase ? () => config.onOpenCase?.(patientData.orderId, patientData.displayOrderId) : void 0;
4189
4258
  const handleJumpToMessage = (messageId) => {
4190
4259
  messageListRef.current?.scrollToMessage(messageId);
4191
4260
  };
@@ -4261,6 +4330,7 @@ function CollabPanel({
4261
4330
  patientData,
4262
4331
  participants,
4263
4332
  onOpenDicom: handleOpenDicom,
4333
+ onOpenCase: handleOpenCase,
4264
4334
  onOpenSettings: () => setShowSettings(true),
4265
4335
  onBack,
4266
4336
  hideName: hidePatientName,
@@ -4515,14 +4585,16 @@ function CollabPopup({
4515
4585
  ),
4516
4586
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleCenter, children: [
4517
4587
  /* @__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
- ] })
4588
+ (() => {
4589
+ const parts = [];
4590
+ if (patientData.patientAge) parts.push(String(patientData.patientAge));
4591
+ if (patientData.patientSex) parts.push(String(patientData.patientSex));
4592
+ if (patientData.studyType) parts.push(patientData.studyType);
4593
+ if (patientData.bodyParts && patientData.bodyParts.length > 0) {
4594
+ parts.push(patientData.bodyParts.join(", "));
4595
+ }
4596
+ return parts.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles13.subRow, children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles13.metaText, children: parts.join(" \xB7 ") }) }) : null;
4597
+ })()
4526
4598
  ] }),
4527
4599
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleActions, children: [
4528
4600
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4611,19 +4683,17 @@ var styles13 = {
4611
4683
  subRow: {
4612
4684
  display: "flex",
4613
4685
  alignItems: "center",
4614
- gap: SPACE.S2,
4615
4686
  fontSize: FONT_SIZE.sm,
4616
4687
  color: COLOR.neutral500,
4617
4688
  overflow: "hidden",
4618
4689
  textOverflow: "ellipsis",
4619
4690
  whiteSpace: "nowrap"
4620
4691
  },
4621
- subDot: {
4622
- color: COLOR.neutral400
4623
- },
4624
- mono: {
4625
- fontFamily: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
4626
- fontVariantNumeric: "tabular-nums"
4692
+ metaText: {
4693
+ overflow: "hidden",
4694
+ textOverflow: "ellipsis",
4695
+ whiteSpace: "nowrap",
4696
+ minWidth: 0
4627
4697
  },
4628
4698
  titleActions: {
4629
4699
  display: "flex",
@@ -5436,7 +5506,7 @@ function ConversationListItem({
5436
5506
  const studyType = item.patientSnapshot?.studyType;
5437
5507
  const patientId = item.patientSnapshot?.patientId;
5438
5508
  const showStudyRow = !!(studyType || patientId);
5439
- const initials = computeInitials2(item.patientSnapshot?.patientName || displayName);
5509
+ const initials = computeInitials(item.patientSnapshot?.patientName || displayName);
5440
5510
  return /* @__PURE__ */ jsxRuntime.jsxs(
5441
5511
  "button",
5442
5512
  {
@@ -5531,7 +5601,7 @@ function previewText(message) {
5531
5601
  }
5532
5602
  }
5533
5603
  }
5534
- function computeInitials2(name) {
5604
+ function computeInitials(name) {
5535
5605
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("") || "?";
5536
5606
  }
5537
5607
  var styles15 = {