@natoe/colab 0.1.14 → 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.d.mts +61 -16
- package/dist/index.d.ts +61 -16
- package/dist/index.js +263 -175
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +263 -175
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -63,6 +63,19 @@ function toCamelKey(key) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// src/core/socket.ts
|
|
66
|
+
function normalizeUnreadPayload(payload) {
|
|
67
|
+
if (payload && typeof payload === "object") {
|
|
68
|
+
const obj = payload;
|
|
69
|
+
if (obj.conversation && typeof obj.conversation === "object") {
|
|
70
|
+
return {
|
|
71
|
+
conversation: obj.conversation,
|
|
72
|
+
order: obj.order && typeof obj.order === "object" ? obj.order : {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { conversation: payload, order: {} };
|
|
76
|
+
}
|
|
77
|
+
return { conversation: {}, order: {} };
|
|
78
|
+
}
|
|
66
79
|
var CollabSocket = class {
|
|
67
80
|
constructor() {
|
|
68
81
|
this.socket = null;
|
|
@@ -128,7 +141,7 @@ var CollabSocket = class {
|
|
|
128
141
|
if (!this.socket || !this.config) return;
|
|
129
142
|
this.userChannel = this.socket.channel("user_notifications", {});
|
|
130
143
|
this.userChannel.on("unread_update", (payload) => {
|
|
131
|
-
this.onUnreadUpdate?.(payload);
|
|
144
|
+
this.onUnreadUpdate?.(normalizeUnreadPayload(payload));
|
|
132
145
|
});
|
|
133
146
|
this.userChannel.join().receive("ok", () => {
|
|
134
147
|
}).receive("error", (reason) => {
|
|
@@ -139,7 +152,9 @@ var CollabSocket = class {
|
|
|
139
152
|
});
|
|
140
153
|
});
|
|
141
154
|
}
|
|
142
|
-
/** Register callback for unread count changes
|
|
155
|
+
/** Register callback for unread count changes. The callback receives
|
|
156
|
+
* the normalised {@link UnreadCounts} shape regardless of which
|
|
157
|
+
* payload format the backend pushed. */
|
|
143
158
|
onUnreadCountUpdate(callback) {
|
|
144
159
|
this.onUnreadUpdate = callback;
|
|
145
160
|
}
|
|
@@ -504,8 +519,6 @@ var SHADOW = {
|
|
|
504
519
|
toast: "0 4px 12px rgba(0, 0, 0, 0.18)"
|
|
505
520
|
};
|
|
506
521
|
var Z_INDEX = {
|
|
507
|
-
/** Sticky day divider — above bubbles, below interactive popovers. */
|
|
508
|
-
sticky: 1,
|
|
509
522
|
/** Toasts inside a panel. */
|
|
510
523
|
toast: 20,
|
|
511
524
|
/** Floating CollabPopup dialog. */
|
|
@@ -583,6 +596,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
583
596
|
return new CollabSocket();
|
|
584
597
|
});
|
|
585
598
|
const [unreadCounts, setUnreadCounts] = React4.useState({});
|
|
599
|
+
const [unreadCountsByOrder, setUnreadCountsByOrder] = React4.useState({});
|
|
586
600
|
const pendingOrderIds = React4.useRef(/* @__PURE__ */ new Set());
|
|
587
601
|
const pendingResolvers = React4.useRef(/* @__PURE__ */ new Map());
|
|
588
602
|
const previewCache = React4.useRef(/* @__PURE__ */ new Map());
|
|
@@ -598,7 +612,8 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
598
612
|
setSocket(s);
|
|
599
613
|
}
|
|
600
614
|
s.onUnreadCountUpdate((counts) => {
|
|
601
|
-
setUnreadCounts(counts);
|
|
615
|
+
setUnreadCounts(counts.conversation);
|
|
616
|
+
setUnreadCountsByOrder(counts.order);
|
|
602
617
|
previewCache.current.clear();
|
|
603
618
|
});
|
|
604
619
|
if (!s.isConnected()) {
|
|
@@ -806,6 +821,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
806
821
|
apiBaseUrl,
|
|
807
822
|
totalUnread,
|
|
808
823
|
unreadCounts,
|
|
824
|
+
unreadCountsByOrder,
|
|
809
825
|
requestPreview,
|
|
810
826
|
invalidatePreview,
|
|
811
827
|
fetchMessages,
|
|
@@ -1434,6 +1450,31 @@ function DicomIcon({ size = 18, color = "currentColor" }) {
|
|
|
1434
1450
|
}
|
|
1435
1451
|
);
|
|
1436
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
|
+
}
|
|
1437
1478
|
function PeopleIcon({ size = 18, color = "currentColor" }) {
|
|
1438
1479
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1439
1480
|
"svg",
|
|
@@ -1471,15 +1512,19 @@ function BackIcon({ size = 22, color = "currentColor" }) {
|
|
|
1471
1512
|
}
|
|
1472
1513
|
);
|
|
1473
1514
|
}
|
|
1515
|
+
function stripOrderIdSuffix(name) {
|
|
1516
|
+
return name.replace(/\s*-\s*\d{1,6}\s*$/, "").trim();
|
|
1517
|
+
}
|
|
1474
1518
|
function resolveDisplayName(patientData, displayName) {
|
|
1475
1519
|
const localComplete = !!patientData.labName && !!patientData.displayOrderId;
|
|
1476
|
-
|
|
1477
|
-
return
|
|
1520
|
+
const raw = localComplete ? buildChannelName(patientData) : cleanChannelName(displayName) || buildChannelName(patientData);
|
|
1521
|
+
return stripOrderIdSuffix(raw);
|
|
1478
1522
|
}
|
|
1479
1523
|
function PatientHeader({
|
|
1480
1524
|
patientData,
|
|
1481
1525
|
participants,
|
|
1482
1526
|
onOpenDicom,
|
|
1527
|
+
onOpenCase,
|
|
1483
1528
|
onOpenSettings,
|
|
1484
1529
|
onBack,
|
|
1485
1530
|
hideName = false,
|
|
@@ -1487,29 +1532,58 @@ function PatientHeader({
|
|
|
1487
1532
|
className
|
|
1488
1533
|
}) {
|
|
1489
1534
|
const hasDicom = !!(patientData.studyId && patientData.storageId);
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
value: `${patientData.patientAge} \xB7 ${patientData.patientSex}`
|
|
1496
|
-
});
|
|
1497
|
-
} else if (patientData.patientAge) {
|
|
1498
|
-
meta.push({ key: "age", label: "Age", value: String(patientData.patientAge) });
|
|
1499
|
-
} else if (patientData.patientSex) {
|
|
1500
|
-
meta.push({ key: "sex", label: "Sex", value: String(patientData.patientSex) });
|
|
1501
|
-
}
|
|
1502
|
-
if (patientData.studyType) {
|
|
1503
|
-
meta.push({ key: "modality", label: "Modality", value: patientData.studyType });
|
|
1504
|
-
}
|
|
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);
|
|
1505
1540
|
if (patientData.bodyParts && patientData.bodyParts.length > 0) {
|
|
1506
|
-
|
|
1507
|
-
key: "body",
|
|
1508
|
-
label: "Body part",
|
|
1509
|
-
value: patientData.bodyParts.join(", ")
|
|
1510
|
-
});
|
|
1541
|
+
metaParts.push(patientData.bodyParts.join(", "));
|
|
1511
1542
|
}
|
|
1512
|
-
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;
|
|
1513
1587
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: styles.container, children: [
|
|
1514
1588
|
!hideName && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.nameRow, children: [
|
|
1515
1589
|
onBack && /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1520,46 +1594,14 @@ function PatientHeader({
|
|
|
1520
1594
|
style: styles.backButton,
|
|
1521
1595
|
"aria-label": "Back to conversations",
|
|
1522
1596
|
title: "Back to conversations",
|
|
1523
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(BackIcon, { size:
|
|
1597
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(BackIcon, { size: 20, color: COLOR.neutral800 })
|
|
1524
1598
|
}
|
|
1525
1599
|
),
|
|
1526
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.nameText, children:
|
|
1600
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.nameText, children: resolvedName }),
|
|
1601
|
+
actions
|
|
1527
1602
|
] }),
|
|
1528
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1529
|
-
|
|
1530
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.metaLabel, children: item.label }),
|
|
1531
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.metaValue, children: item.value })
|
|
1532
|
-
] }, item.key)) }),
|
|
1533
|
-
hasActions && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.actions, children: [
|
|
1534
|
-
hasDicom && onOpenDicom && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1535
|
-
"button",
|
|
1536
|
-
{
|
|
1537
|
-
onClick: onOpenDicom,
|
|
1538
|
-
style: styles.dicomButton,
|
|
1539
|
-
type: "button",
|
|
1540
|
-
"aria-label": "View DICOM study",
|
|
1541
|
-
children: [
|
|
1542
|
-
/* @__PURE__ */ jsxRuntime.jsx(DicomIcon, { size: 18, color: COLOR.primary }),
|
|
1543
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "View DICOM" })
|
|
1544
|
-
]
|
|
1545
|
-
}
|
|
1546
|
-
),
|
|
1547
|
-
onOpenSettings && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1548
|
-
"button",
|
|
1549
|
-
{
|
|
1550
|
-
onClick: onOpenSettings,
|
|
1551
|
-
style: styles.settingsIconButton,
|
|
1552
|
-
type: "button",
|
|
1553
|
-
"aria-label": `Open channel settings (${participants.length} participants)`,
|
|
1554
|
-
title: "Channel participants",
|
|
1555
|
-
children: [
|
|
1556
|
-
/* @__PURE__ */ jsxRuntime.jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
|
|
1557
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.participantCount, children: participants.length })
|
|
1558
|
-
]
|
|
1559
|
-
}
|
|
1560
|
-
)
|
|
1561
|
-
] })
|
|
1562
|
-
] })
|
|
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 })
|
|
1563
1605
|
] });
|
|
1564
1606
|
}
|
|
1565
1607
|
var styles = {
|
|
@@ -1567,16 +1609,16 @@ var styles = {
|
|
|
1567
1609
|
display: "flex",
|
|
1568
1610
|
flexDirection: "column",
|
|
1569
1611
|
backgroundColor: COLOR.primaryBg,
|
|
1570
|
-
borderBottom: `1px solid ${COLOR.neutral200}
|
|
1612
|
+
borderBottom: `1px solid ${COLOR.neutral200}`,
|
|
1613
|
+
padding: `${SPACE.S2} ${SPACE.S4}`,
|
|
1614
|
+
gap: "2px"
|
|
1571
1615
|
},
|
|
1572
|
-
//
|
|
1616
|
+
// Title row — back · name · actions in one horizontal line.
|
|
1573
1617
|
nameRow: {
|
|
1574
1618
|
display: "flex",
|
|
1575
1619
|
alignItems: "center",
|
|
1576
1620
|
gap: SPACE.S2,
|
|
1577
|
-
|
|
1578
|
-
borderBottom: `1px solid ${COLOR.neutral200}`,
|
|
1579
|
-
backgroundColor: COLOR.white
|
|
1621
|
+
minWidth: 0
|
|
1580
1622
|
},
|
|
1581
1623
|
backButton: {
|
|
1582
1624
|
width: SIZE.controlIcon,
|
|
@@ -1592,57 +1634,45 @@ var styles = {
|
|
|
1592
1634
|
flexShrink: 0
|
|
1593
1635
|
},
|
|
1594
1636
|
nameText: {
|
|
1637
|
+
flex: 1,
|
|
1638
|
+
minWidth: 0,
|
|
1595
1639
|
fontSize: FONT_SIZE.lg,
|
|
1596
1640
|
fontWeight: FONT_WEIGHT.bold,
|
|
1597
1641
|
color: COLOR.neutral900,
|
|
1598
1642
|
lineHeight: LINE_HEIGHT.tight,
|
|
1599
1643
|
letterSpacing: "-0.01em",
|
|
1600
|
-
minWidth: 0,
|
|
1601
1644
|
overflow: "hidden",
|
|
1602
1645
|
textOverflow: "ellipsis",
|
|
1603
1646
|
whiteSpace: "nowrap"
|
|
1604
1647
|
},
|
|
1605
|
-
//
|
|
1606
|
-
//
|
|
1607
|
-
//
|
|
1608
|
-
caseCard: {
|
|
1609
|
-
display: "flex",
|
|
1610
|
-
flexDirection: "column",
|
|
1611
|
-
gap: SPACE.S3,
|
|
1612
|
-
padding: `${SPACE.S3} ${SPACE.S5}`
|
|
1613
|
-
},
|
|
1614
|
-
// Wrap-row of stacked label/value columns. Compact column gap keeps
|
|
1615
|
-
// the row dense without crowding; row gap kicks in when columns
|
|
1616
|
-
// 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.
|
|
1617
1651
|
metaRow: {
|
|
1618
1652
|
display: "flex",
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
rowGap: SPACE.S2,
|
|
1653
|
+
alignItems: "center",
|
|
1654
|
+
paddingLeft: `calc(${SIZE.controlIcon} + ${SPACE.S2} - ${SPACE.S2})`,
|
|
1622
1655
|
minWidth: 0
|
|
1623
1656
|
},
|
|
1624
|
-
|
|
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: {
|
|
1625
1663
|
display: "flex",
|
|
1626
|
-
|
|
1627
|
-
gap: "2px",
|
|
1664
|
+
justifyContent: "flex-start",
|
|
1628
1665
|
minWidth: 0
|
|
1629
1666
|
},
|
|
1630
|
-
|
|
1631
|
-
fontSize: "11px",
|
|
1632
|
-
fontWeight: FONT_WEIGHT.bold,
|
|
1633
|
-
letterSpacing: "0.06em",
|
|
1634
|
-
textTransform: "uppercase",
|
|
1635
|
-
color: COLOR.neutral500,
|
|
1636
|
-
whiteSpace: "nowrap"
|
|
1637
|
-
},
|
|
1638
|
-
metaValue: {
|
|
1667
|
+
metaText: {
|
|
1639
1668
|
fontSize: FONT_SIZE.sm,
|
|
1640
|
-
fontWeight: FONT_WEIGHT.
|
|
1641
|
-
color: COLOR.
|
|
1642
|
-
|
|
1669
|
+
fontWeight: FONT_WEIGHT.medium,
|
|
1670
|
+
color: COLOR.neutral600,
|
|
1671
|
+
lineHeight: LINE_HEIGHT.tight,
|
|
1643
1672
|
overflow: "hidden",
|
|
1644
1673
|
textOverflow: "ellipsis",
|
|
1645
|
-
|
|
1674
|
+
whiteSpace: "nowrap",
|
|
1675
|
+
minWidth: 0
|
|
1646
1676
|
},
|
|
1647
1677
|
actions: {
|
|
1648
1678
|
display: "flex",
|
|
@@ -1654,7 +1684,7 @@ var styles = {
|
|
|
1654
1684
|
alignItems: "center",
|
|
1655
1685
|
gap: SPACE.S2,
|
|
1656
1686
|
minHeight: SIZE.control,
|
|
1657
|
-
padding: `${SPACE.
|
|
1687
|
+
padding: `${SPACE.S1} ${SPACE.S3}`,
|
|
1658
1688
|
fontSize: FONT_SIZE.sm,
|
|
1659
1689
|
fontWeight: FONT_WEIGHT.semibold,
|
|
1660
1690
|
color: COLOR.primary,
|
|
@@ -1663,10 +1693,25 @@ var styles = {
|
|
|
1663
1693
|
borderRadius: RADIUS.lg,
|
|
1664
1694
|
cursor: "pointer"
|
|
1665
1695
|
},
|
|
1666
|
-
//
|
|
1667
|
-
//
|
|
1668
|
-
//
|
|
1669
|
-
|
|
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}`,
|
|
1705
|
+
fontSize: FONT_SIZE.sm,
|
|
1706
|
+
fontWeight: FONT_WEIGHT.semibold,
|
|
1707
|
+
color: COLOR.primary,
|
|
1708
|
+
backgroundColor: COLOR.white,
|
|
1709
|
+
border: `1.5px solid ${COLOR.primary}`,
|
|
1710
|
+
borderRadius: RADIUS.lg,
|
|
1711
|
+
cursor: "pointer"
|
|
1712
|
+
},
|
|
1713
|
+
// Channel-info button: people icon + participant count. Opens the
|
|
1714
|
+
// channel settings overlay.
|
|
1670
1715
|
settingsIconButton: {
|
|
1671
1716
|
minHeight: SIZE.control,
|
|
1672
1717
|
display: "inline-flex",
|
|
@@ -2146,6 +2191,62 @@ function AlertIcon({ size = 14, color = "currentColor" }) {
|
|
|
2146
2191
|
}
|
|
2147
2192
|
);
|
|
2148
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
|
+
}
|
|
2149
2250
|
function Spinner({ size = 16, color = COLOR.neutral500, thickness = 2 }) {
|
|
2150
2251
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2151
2252
|
"span",
|
|
@@ -2234,12 +2335,11 @@ function MessageBubble({
|
|
|
2234
2335
|
...styles5.authorAvatar,
|
|
2235
2336
|
backgroundColor: roleColor(message.senderRole)
|
|
2236
2337
|
},
|
|
2237
|
-
"aria-
|
|
2238
|
-
children:
|
|
2338
|
+
"aria-label": ROLE_LABELS[message.senderRole] ?? message.senderRole,
|
|
2339
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(RoleAvatarIcon, { role: message.senderRole, size: 20, color: COLOR.white })
|
|
2239
2340
|
}
|
|
2240
2341
|
),
|
|
2241
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.authorName, children: message.senderName })
|
|
2242
|
-
/* @__PURE__ */ jsxRuntime.jsx(RoleBadge, { role: message.senderRole })
|
|
2342
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.authorName, children: message.senderName })
|
|
2243
2343
|
] }),
|
|
2244
2344
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2245
2345
|
"div",
|
|
@@ -2314,9 +2414,6 @@ function MessageBubble({
|
|
|
2314
2414
|
}
|
|
2315
2415
|
);
|
|
2316
2416
|
}
|
|
2317
|
-
function computeInitials(name) {
|
|
2318
|
-
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
|
|
2319
|
-
}
|
|
2320
2417
|
function roleColor(role) {
|
|
2321
2418
|
switch (role) {
|
|
2322
2419
|
case "radiologist":
|
|
@@ -2401,9 +2498,6 @@ function SystemBubble({ message }) {
|
|
|
2401
2498
|
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles5.systemTime, children: formatTime(message.insertedAt) })
|
|
2402
2499
|
] }) });
|
|
2403
2500
|
}
|
|
2404
|
-
function RoleBadge({ role }) {
|
|
2405
|
-
return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { ...styles5.roleBadge, backgroundColor: roleColor(role) }, children: ROLE_LABELS[role] ?? role });
|
|
2406
|
-
}
|
|
2407
2501
|
function formatTime(iso) {
|
|
2408
2502
|
try {
|
|
2409
2503
|
return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
@@ -2505,16 +2599,6 @@ var styles5 = {
|
|
|
2505
2599
|
color: "#fff4d6",
|
|
2506
2600
|
backgroundColor: "rgba(255, 255, 255, 0.2)"
|
|
2507
2601
|
},
|
|
2508
|
-
roleBadge: {
|
|
2509
|
-
fontSize: "10px",
|
|
2510
|
-
fontWeight: FONT_WEIGHT.bold,
|
|
2511
|
-
padding: "2px 7px",
|
|
2512
|
-
borderRadius: RADIUS.sm,
|
|
2513
|
-
textTransform: "uppercase",
|
|
2514
|
-
letterSpacing: "0.06em",
|
|
2515
|
-
color: COLOR.white,
|
|
2516
|
-
flexShrink: 0
|
|
2517
|
-
},
|
|
2518
2602
|
textBody: {
|
|
2519
2603
|
margin: 0,
|
|
2520
2604
|
fontSize: FONT_SIZE.md,
|
|
@@ -2978,9 +3062,6 @@ var styles6 = {
|
|
|
2978
3062
|
alignItems: "center",
|
|
2979
3063
|
gap: SPACE.S3,
|
|
2980
3064
|
margin: `${SPACE.S4} 0 ${SPACE.S3}`,
|
|
2981
|
-
position: "sticky",
|
|
2982
|
-
top: SPACE.S1,
|
|
2983
|
-
zIndex: Z_INDEX.sticky,
|
|
2984
3065
|
pointerEvents: "none"
|
|
2985
3066
|
},
|
|
2986
3067
|
dayHr: {
|
|
@@ -2995,11 +3076,7 @@ var styles6 = {
|
|
|
2995
3076
|
color: COLOR.neutral500,
|
|
2996
3077
|
textTransform: "uppercase",
|
|
2997
3078
|
letterSpacing: "0.06em",
|
|
2998
|
-
// Subtle pill background so the label is readable when it overlays
|
|
2999
|
-
// bubbles passing under it during sticky scroll. Without this the
|
|
3000
|
-
// hairlines visually run through the text on tinted backgrounds.
|
|
3001
3079
|
padding: `2px ${SPACE.S2}`,
|
|
3002
|
-
backgroundColor: COLOR.white,
|
|
3003
3080
|
borderRadius: RADIUS.sm
|
|
3004
3081
|
},
|
|
3005
3082
|
bubbleSlot: {
|
|
@@ -3297,7 +3374,7 @@ function MessageInput({
|
|
|
3297
3374
|
style: styles8.textarea
|
|
3298
3375
|
}
|
|
3299
3376
|
),
|
|
3300
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3377
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3301
3378
|
"button",
|
|
3302
3379
|
{
|
|
3303
3380
|
onClick: handleSendText,
|
|
@@ -3309,10 +3386,7 @@ function MessageInput({
|
|
|
3309
3386
|
opacity: text.trim() ? 1 : 0.5
|
|
3310
3387
|
},
|
|
3311
3388
|
type: "button",
|
|
3312
|
-
children:
|
|
3313
|
-
/* @__PURE__ */ jsxRuntime.jsx(SendIcon, { size: 20, color: COLOR.white }),
|
|
3314
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles8.sendLabel, children: "Send" })
|
|
3315
|
-
]
|
|
3389
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(SendIcon, { size: 20, color: COLOR.white })
|
|
3316
3390
|
}
|
|
3317
3391
|
)
|
|
3318
3392
|
] })
|
|
@@ -3410,29 +3484,33 @@ var styles8 = {
|
|
|
3410
3484
|
fontFamily: "inherit",
|
|
3411
3485
|
transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease"
|
|
3412
3486
|
},
|
|
3413
|
-
//
|
|
3414
|
-
//
|
|
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.
|
|
3415
3490
|
sendButton: {
|
|
3416
3491
|
display: "inline-flex",
|
|
3417
3492
|
alignItems: "center",
|
|
3418
|
-
|
|
3419
|
-
|
|
3493
|
+
justifyContent: "center",
|
|
3494
|
+
width: "48px",
|
|
3420
3495
|
height: "48px",
|
|
3421
|
-
padding: `0 ${SPACE.S4}`,
|
|
3422
3496
|
backgroundColor: COLOR.primary,
|
|
3423
3497
|
color: COLOR.white,
|
|
3424
3498
|
border: "none",
|
|
3425
3499
|
borderRadius: "14px",
|
|
3426
3500
|
cursor: "pointer",
|
|
3427
3501
|
flexShrink: 0,
|
|
3428
|
-
fontFamily: "inherit",
|
|
3429
|
-
fontSize: "15px",
|
|
3430
|
-
fontWeight: FONT_WEIGHT.semibold,
|
|
3431
3502
|
transition: "opacity 120ms ease, background-color 120ms ease"
|
|
3432
|
-
},
|
|
3433
|
-
sendLabel: {
|
|
3434
|
-
lineHeight: 1
|
|
3435
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
|
+
};
|
|
3436
3514
|
function ParticipantsList({
|
|
3437
3515
|
participants,
|
|
3438
3516
|
currentUserId,
|
|
@@ -3442,7 +3520,17 @@ function ParticipantsList({
|
|
|
3442
3520
|
}) {
|
|
3443
3521
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style: styles9.list, children: participants.map((participant) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles9.item, children: [
|
|
3444
3522
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles9.avatar, children: [
|
|
3445
|
-
participant.avatar ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
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
|
+
),
|
|
3446
3534
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3447
3535
|
"span",
|
|
3448
3536
|
{
|
|
@@ -3458,7 +3546,7 @@ function ParticipantsList({
|
|
|
3458
3546
|
participant.userName ?? fallbackName(participant.userRole),
|
|
3459
3547
|
participant.userId === currentUserId && /* @__PURE__ */ jsxRuntime.jsx("span", { style: styles9.youBadge, children: " (you)" })
|
|
3460
3548
|
] }),
|
|
3461
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3549
|
+
/* @__PURE__ */ jsxRuntime.jsx(RoleBadge, { role: participant.userRole })
|
|
3462
3550
|
] }),
|
|
3463
3551
|
isAdmin && participant.userId !== currentUserId && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3464
3552
|
"button",
|
|
@@ -3486,7 +3574,7 @@ function fallbackName(role) {
|
|
|
3486
3574
|
return "User";
|
|
3487
3575
|
}
|
|
3488
3576
|
}
|
|
3489
|
-
function
|
|
3577
|
+
function RoleBadge({ role }) {
|
|
3490
3578
|
const colors = {
|
|
3491
3579
|
radiologist: { bg: "#eef2ff", text: "#4f46e5" },
|
|
3492
3580
|
// indigo
|
|
@@ -3528,13 +3616,10 @@ var styles9 = {
|
|
|
3528
3616
|
width: "32px",
|
|
3529
3617
|
height: "32px",
|
|
3530
3618
|
borderRadius: RADIUS.full,
|
|
3531
|
-
backgroundColor: COLOR.neutral200,
|
|
3532
3619
|
display: "flex",
|
|
3533
3620
|
alignItems: "center",
|
|
3534
3621
|
justifyContent: "center",
|
|
3535
|
-
|
|
3536
|
-
fontWeight: FONT_WEIGHT.semibold,
|
|
3537
|
-
color: COLOR.neutral500
|
|
3622
|
+
color: COLOR.white
|
|
3538
3623
|
},
|
|
3539
3624
|
statusDot: {
|
|
3540
3625
|
position: "absolute",
|
|
@@ -4126,6 +4211,7 @@ function CollabPanel({
|
|
|
4126
4211
|
showSeenBy = true,
|
|
4127
4212
|
onBack,
|
|
4128
4213
|
hidePatientName = false,
|
|
4214
|
+
hideOpenCase = false,
|
|
4129
4215
|
onConversationChange,
|
|
4130
4216
|
themeMode = "light",
|
|
4131
4217
|
className,
|
|
@@ -4168,6 +4254,7 @@ function CollabPanel({
|
|
|
4168
4254
|
config.onOpenDicom(patientData.studyId, patientData.storageId);
|
|
4169
4255
|
}
|
|
4170
4256
|
};
|
|
4257
|
+
const handleOpenCase = !hideOpenCase && config.onOpenCase ? () => config.onOpenCase?.(patientData.orderId, patientData.displayOrderId) : void 0;
|
|
4171
4258
|
const handleJumpToMessage = (messageId) => {
|
|
4172
4259
|
messageListRef.current?.scrollToMessage(messageId);
|
|
4173
4260
|
};
|
|
@@ -4243,6 +4330,7 @@ function CollabPanel({
|
|
|
4243
4330
|
patientData,
|
|
4244
4331
|
participants,
|
|
4245
4332
|
onOpenDicom: handleOpenDicom,
|
|
4333
|
+
onOpenCase: handleOpenCase,
|
|
4246
4334
|
onOpenSettings: () => setShowSettings(true),
|
|
4247
4335
|
onBack,
|
|
4248
4336
|
hideName: hidePatientName,
|
|
@@ -4497,14 +4585,16 @@ function CollabPopup({
|
|
|
4497
4585
|
),
|
|
4498
4586
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleCenter, children: [
|
|
4499
4587
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { id: titleId, style: styles13.patientName, children: patientData.patientName || titleText }),
|
|
4500
|
-
(
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
patientData.
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
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
|
+
})()
|
|
4508
4598
|
] }),
|
|
4509
4599
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles13.titleActions, children: [
|
|
4510
4600
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -4593,19 +4683,17 @@ var styles13 = {
|
|
|
4593
4683
|
subRow: {
|
|
4594
4684
|
display: "flex",
|
|
4595
4685
|
alignItems: "center",
|
|
4596
|
-
gap: SPACE.S2,
|
|
4597
4686
|
fontSize: FONT_SIZE.sm,
|
|
4598
4687
|
color: COLOR.neutral500,
|
|
4599
4688
|
overflow: "hidden",
|
|
4600
4689
|
textOverflow: "ellipsis",
|
|
4601
4690
|
whiteSpace: "nowrap"
|
|
4602
4691
|
},
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
fontVariantNumeric: "tabular-nums"
|
|
4692
|
+
metaText: {
|
|
4693
|
+
overflow: "hidden",
|
|
4694
|
+
textOverflow: "ellipsis",
|
|
4695
|
+
whiteSpace: "nowrap",
|
|
4696
|
+
minWidth: 0
|
|
4609
4697
|
},
|
|
4610
4698
|
titleActions: {
|
|
4611
4699
|
display: "flex",
|
|
@@ -5418,7 +5506,7 @@ function ConversationListItem({
|
|
|
5418
5506
|
const studyType = item.patientSnapshot?.studyType;
|
|
5419
5507
|
const patientId = item.patientSnapshot?.patientId;
|
|
5420
5508
|
const showStudyRow = !!(studyType || patientId);
|
|
5421
|
-
const initials =
|
|
5509
|
+
const initials = computeInitials(item.patientSnapshot?.patientName || displayName);
|
|
5422
5510
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5423
5511
|
"button",
|
|
5424
5512
|
{
|
|
@@ -5513,7 +5601,7 @@ function previewText(message) {
|
|
|
5513
5601
|
}
|
|
5514
5602
|
}
|
|
5515
5603
|
}
|
|
5516
|
-
function
|
|
5604
|
+
function computeInitials(name) {
|
|
5517
5605
|
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("") || "?";
|
|
5518
5606
|
}
|
|
5519
5607
|
var styles15 = {
|
|
@@ -6327,7 +6415,7 @@ function useUnreadCount() {
|
|
|
6327
6415
|
const [counts, setCounts] = React4.useState({});
|
|
6328
6416
|
React4.useEffect(() => {
|
|
6329
6417
|
socket.onUnreadCountUpdate((serverCounts) => {
|
|
6330
|
-
setCounts(serverCounts);
|
|
6418
|
+
setCounts(serverCounts.conversation);
|
|
6331
6419
|
});
|
|
6332
6420
|
}, [socket]);
|
|
6333
6421
|
const getCountForConversation = React4.useCallback(
|