@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.mjs
CHANGED
|
@@ -57,6 +57,19 @@ function toCamelKey(key) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// src/core/socket.ts
|
|
60
|
+
function normalizeUnreadPayload(payload) {
|
|
61
|
+
if (payload && typeof payload === "object") {
|
|
62
|
+
const obj = payload;
|
|
63
|
+
if (obj.conversation && typeof obj.conversation === "object") {
|
|
64
|
+
return {
|
|
65
|
+
conversation: obj.conversation,
|
|
66
|
+
order: obj.order && typeof obj.order === "object" ? obj.order : {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { conversation: payload, order: {} };
|
|
70
|
+
}
|
|
71
|
+
return { conversation: {}, order: {} };
|
|
72
|
+
}
|
|
60
73
|
var CollabSocket = class {
|
|
61
74
|
constructor() {
|
|
62
75
|
this.socket = null;
|
|
@@ -122,7 +135,7 @@ var CollabSocket = class {
|
|
|
122
135
|
if (!this.socket || !this.config) return;
|
|
123
136
|
this.userChannel = this.socket.channel("user_notifications", {});
|
|
124
137
|
this.userChannel.on("unread_update", (payload) => {
|
|
125
|
-
this.onUnreadUpdate?.(payload);
|
|
138
|
+
this.onUnreadUpdate?.(normalizeUnreadPayload(payload));
|
|
126
139
|
});
|
|
127
140
|
this.userChannel.join().receive("ok", () => {
|
|
128
141
|
}).receive("error", (reason) => {
|
|
@@ -133,7 +146,9 @@ var CollabSocket = class {
|
|
|
133
146
|
});
|
|
134
147
|
});
|
|
135
148
|
}
|
|
136
|
-
/** Register callback for unread count changes
|
|
149
|
+
/** Register callback for unread count changes. The callback receives
|
|
150
|
+
* the normalised {@link UnreadCounts} shape regardless of which
|
|
151
|
+
* payload format the backend pushed. */
|
|
137
152
|
onUnreadCountUpdate(callback) {
|
|
138
153
|
this.onUnreadUpdate = callback;
|
|
139
154
|
}
|
|
@@ -498,8 +513,6 @@ var SHADOW = {
|
|
|
498
513
|
toast: "0 4px 12px rgba(0, 0, 0, 0.18)"
|
|
499
514
|
};
|
|
500
515
|
var Z_INDEX = {
|
|
501
|
-
/** Sticky day divider — above bubbles, below interactive popovers. */
|
|
502
|
-
sticky: 1,
|
|
503
516
|
/** Toasts inside a panel. */
|
|
504
517
|
toast: 20,
|
|
505
518
|
/** Floating CollabPopup dialog. */
|
|
@@ -577,6 +590,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
577
590
|
return new CollabSocket();
|
|
578
591
|
});
|
|
579
592
|
const [unreadCounts, setUnreadCounts] = useState({});
|
|
593
|
+
const [unreadCountsByOrder, setUnreadCountsByOrder] = useState({});
|
|
580
594
|
const pendingOrderIds = useRef(/* @__PURE__ */ new Set());
|
|
581
595
|
const pendingResolvers = useRef(/* @__PURE__ */ new Map());
|
|
582
596
|
const previewCache = useRef(/* @__PURE__ */ new Map());
|
|
@@ -592,7 +606,8 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
592
606
|
setSocket(s);
|
|
593
607
|
}
|
|
594
608
|
s.onUnreadCountUpdate((counts) => {
|
|
595
|
-
setUnreadCounts(counts);
|
|
609
|
+
setUnreadCounts(counts.conversation);
|
|
610
|
+
setUnreadCountsByOrder(counts.order);
|
|
596
611
|
previewCache.current.clear();
|
|
597
612
|
});
|
|
598
613
|
if (!s.isConnected()) {
|
|
@@ -800,6 +815,7 @@ function CollabProvider({ config, apiBaseUrl, children }) {
|
|
|
800
815
|
apiBaseUrl,
|
|
801
816
|
totalUnread,
|
|
802
817
|
unreadCounts,
|
|
818
|
+
unreadCountsByOrder,
|
|
803
819
|
requestPreview,
|
|
804
820
|
invalidatePreview,
|
|
805
821
|
fetchMessages,
|
|
@@ -1428,6 +1444,31 @@ function DicomIcon({ size = 18, color = "currentColor" }) {
|
|
|
1428
1444
|
}
|
|
1429
1445
|
);
|
|
1430
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
|
+
}
|
|
1431
1472
|
function PeopleIcon({ size = 18, color = "currentColor" }) {
|
|
1432
1473
|
return /* @__PURE__ */ jsxs(
|
|
1433
1474
|
"svg",
|
|
@@ -1465,15 +1506,19 @@ function BackIcon({ size = 22, color = "currentColor" }) {
|
|
|
1465
1506
|
}
|
|
1466
1507
|
);
|
|
1467
1508
|
}
|
|
1509
|
+
function stripOrderIdSuffix(name) {
|
|
1510
|
+
return name.replace(/\s*-\s*\d{1,6}\s*$/, "").trim();
|
|
1511
|
+
}
|
|
1468
1512
|
function resolveDisplayName(patientData, displayName) {
|
|
1469
1513
|
const localComplete = !!patientData.labName && !!patientData.displayOrderId;
|
|
1470
|
-
|
|
1471
|
-
return
|
|
1514
|
+
const raw = localComplete ? buildChannelName(patientData) : cleanChannelName(displayName) || buildChannelName(patientData);
|
|
1515
|
+
return stripOrderIdSuffix(raw);
|
|
1472
1516
|
}
|
|
1473
1517
|
function PatientHeader({
|
|
1474
1518
|
patientData,
|
|
1475
1519
|
participants,
|
|
1476
1520
|
onOpenDicom,
|
|
1521
|
+
onOpenCase,
|
|
1477
1522
|
onOpenSettings,
|
|
1478
1523
|
onBack,
|
|
1479
1524
|
hideName = false,
|
|
@@ -1481,29 +1526,58 @@ function PatientHeader({
|
|
|
1481
1526
|
className
|
|
1482
1527
|
}) {
|
|
1483
1528
|
const hasDicom = !!(patientData.studyId && patientData.storageId);
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
value: `${patientData.patientAge} \xB7 ${patientData.patientSex}`
|
|
1490
|
-
});
|
|
1491
|
-
} else if (patientData.patientAge) {
|
|
1492
|
-
meta.push({ key: "age", label: "Age", value: String(patientData.patientAge) });
|
|
1493
|
-
} else if (patientData.patientSex) {
|
|
1494
|
-
meta.push({ key: "sex", label: "Sex", value: String(patientData.patientSex) });
|
|
1495
|
-
}
|
|
1496
|
-
if (patientData.studyType) {
|
|
1497
|
-
meta.push({ key: "modality", label: "Modality", value: patientData.studyType });
|
|
1498
|
-
}
|
|
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);
|
|
1499
1534
|
if (patientData.bodyParts && patientData.bodyParts.length > 0) {
|
|
1500
|
-
|
|
1501
|
-
key: "body",
|
|
1502
|
-
label: "Body part",
|
|
1503
|
-
value: patientData.bodyParts.join(", ")
|
|
1504
|
-
});
|
|
1535
|
+
metaParts.push(patientData.bodyParts.join(", "));
|
|
1505
1536
|
}
|
|
1506
|
-
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;
|
|
1507
1581
|
return /* @__PURE__ */ jsxs("div", { className, style: styles.container, children: [
|
|
1508
1582
|
!hideName && /* @__PURE__ */ jsxs("div", { style: styles.nameRow, children: [
|
|
1509
1583
|
onBack && /* @__PURE__ */ jsx(
|
|
@@ -1514,46 +1588,14 @@ function PatientHeader({
|
|
|
1514
1588
|
style: styles.backButton,
|
|
1515
1589
|
"aria-label": "Back to conversations",
|
|
1516
1590
|
title: "Back to conversations",
|
|
1517
|
-
children: /* @__PURE__ */ jsx(BackIcon, { size:
|
|
1591
|
+
children: /* @__PURE__ */ jsx(BackIcon, { size: 20, color: COLOR.neutral800 })
|
|
1518
1592
|
}
|
|
1519
1593
|
),
|
|
1520
|
-
/* @__PURE__ */ jsx("span", { style: styles.nameText, children:
|
|
1594
|
+
/* @__PURE__ */ jsx("span", { style: styles.nameText, children: resolvedName }),
|
|
1595
|
+
actions
|
|
1521
1596
|
] }),
|
|
1522
|
-
/* @__PURE__ */
|
|
1523
|
-
|
|
1524
|
-
/* @__PURE__ */ jsx("span", { style: styles.metaLabel, children: item.label }),
|
|
1525
|
-
/* @__PURE__ */ jsx("span", { style: styles.metaValue, children: item.value })
|
|
1526
|
-
] }, item.key)) }),
|
|
1527
|
-
hasActions && /* @__PURE__ */ jsxs("div", { style: styles.actions, children: [
|
|
1528
|
-
hasDicom && onOpenDicom && /* @__PURE__ */ jsxs(
|
|
1529
|
-
"button",
|
|
1530
|
-
{
|
|
1531
|
-
onClick: onOpenDicom,
|
|
1532
|
-
style: styles.dicomButton,
|
|
1533
|
-
type: "button",
|
|
1534
|
-
"aria-label": "View DICOM study",
|
|
1535
|
-
children: [
|
|
1536
|
-
/* @__PURE__ */ jsx(DicomIcon, { size: 18, color: COLOR.primary }),
|
|
1537
|
-
/* @__PURE__ */ jsx("span", { children: "View DICOM" })
|
|
1538
|
-
]
|
|
1539
|
-
}
|
|
1540
|
-
),
|
|
1541
|
-
onOpenSettings && /* @__PURE__ */ jsxs(
|
|
1542
|
-
"button",
|
|
1543
|
-
{
|
|
1544
|
-
onClick: onOpenSettings,
|
|
1545
|
-
style: styles.settingsIconButton,
|
|
1546
|
-
type: "button",
|
|
1547
|
-
"aria-label": `Open channel settings (${participants.length} participants)`,
|
|
1548
|
-
title: "Channel participants",
|
|
1549
|
-
children: [
|
|
1550
|
-
/* @__PURE__ */ jsx(PeopleIcon, { size: 18, color: COLOR.neutral600 }),
|
|
1551
|
-
/* @__PURE__ */ jsx("span", { style: styles.participantCount, children: participants.length })
|
|
1552
|
-
]
|
|
1553
|
-
}
|
|
1554
|
-
)
|
|
1555
|
-
] })
|
|
1556
|
-
] })
|
|
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 })
|
|
1557
1599
|
] });
|
|
1558
1600
|
}
|
|
1559
1601
|
var styles = {
|
|
@@ -1561,16 +1603,16 @@ var styles = {
|
|
|
1561
1603
|
display: "flex",
|
|
1562
1604
|
flexDirection: "column",
|
|
1563
1605
|
backgroundColor: COLOR.primaryBg,
|
|
1564
|
-
borderBottom: `1px solid ${COLOR.neutral200}
|
|
1606
|
+
borderBottom: `1px solid ${COLOR.neutral200}`,
|
|
1607
|
+
padding: `${SPACE.S2} ${SPACE.S4}`,
|
|
1608
|
+
gap: "2px"
|
|
1565
1609
|
},
|
|
1566
|
-
//
|
|
1610
|
+
// Title row — back · name · actions in one horizontal line.
|
|
1567
1611
|
nameRow: {
|
|
1568
1612
|
display: "flex",
|
|
1569
1613
|
alignItems: "center",
|
|
1570
1614
|
gap: SPACE.S2,
|
|
1571
|
-
|
|
1572
|
-
borderBottom: `1px solid ${COLOR.neutral200}`,
|
|
1573
|
-
backgroundColor: COLOR.white
|
|
1615
|
+
minWidth: 0
|
|
1574
1616
|
},
|
|
1575
1617
|
backButton: {
|
|
1576
1618
|
width: SIZE.controlIcon,
|
|
@@ -1586,57 +1628,45 @@ var styles = {
|
|
|
1586
1628
|
flexShrink: 0
|
|
1587
1629
|
},
|
|
1588
1630
|
nameText: {
|
|
1631
|
+
flex: 1,
|
|
1632
|
+
minWidth: 0,
|
|
1589
1633
|
fontSize: FONT_SIZE.lg,
|
|
1590
1634
|
fontWeight: FONT_WEIGHT.bold,
|
|
1591
1635
|
color: COLOR.neutral900,
|
|
1592
1636
|
lineHeight: LINE_HEIGHT.tight,
|
|
1593
1637
|
letterSpacing: "-0.01em",
|
|
1594
|
-
minWidth: 0,
|
|
1595
1638
|
overflow: "hidden",
|
|
1596
1639
|
textOverflow: "ellipsis",
|
|
1597
1640
|
whiteSpace: "nowrap"
|
|
1598
1641
|
},
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1601
|
-
//
|
|
1602
|
-
caseCard: {
|
|
1603
|
-
display: "flex",
|
|
1604
|
-
flexDirection: "column",
|
|
1605
|
-
gap: SPACE.S3,
|
|
1606
|
-
padding: `${SPACE.S3} ${SPACE.S5}`
|
|
1607
|
-
},
|
|
1608
|
-
// Wrap-row of stacked label/value columns. Compact column gap keeps
|
|
1609
|
-
// the row dense without crowding; row gap kicks in when columns
|
|
1610
|
-
// 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.
|
|
1611
1645
|
metaRow: {
|
|
1612
1646
|
display: "flex",
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
rowGap: SPACE.S2,
|
|
1647
|
+
alignItems: "center",
|
|
1648
|
+
paddingLeft: `calc(${SIZE.controlIcon} + ${SPACE.S2} - ${SPACE.S2})`,
|
|
1616
1649
|
minWidth: 0
|
|
1617
1650
|
},
|
|
1618
|
-
|
|
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: {
|
|
1619
1657
|
display: "flex",
|
|
1620
|
-
|
|
1621
|
-
gap: "2px",
|
|
1658
|
+
justifyContent: "flex-start",
|
|
1622
1659
|
minWidth: 0
|
|
1623
1660
|
},
|
|
1624
|
-
|
|
1625
|
-
fontSize: "11px",
|
|
1626
|
-
fontWeight: FONT_WEIGHT.bold,
|
|
1627
|
-
letterSpacing: "0.06em",
|
|
1628
|
-
textTransform: "uppercase",
|
|
1629
|
-
color: COLOR.neutral500,
|
|
1630
|
-
whiteSpace: "nowrap"
|
|
1631
|
-
},
|
|
1632
|
-
metaValue: {
|
|
1661
|
+
metaText: {
|
|
1633
1662
|
fontSize: FONT_SIZE.sm,
|
|
1634
|
-
fontWeight: FONT_WEIGHT.
|
|
1635
|
-
color: COLOR.
|
|
1636
|
-
|
|
1663
|
+
fontWeight: FONT_WEIGHT.medium,
|
|
1664
|
+
color: COLOR.neutral600,
|
|
1665
|
+
lineHeight: LINE_HEIGHT.tight,
|
|
1637
1666
|
overflow: "hidden",
|
|
1638
1667
|
textOverflow: "ellipsis",
|
|
1639
|
-
|
|
1668
|
+
whiteSpace: "nowrap",
|
|
1669
|
+
minWidth: 0
|
|
1640
1670
|
},
|
|
1641
1671
|
actions: {
|
|
1642
1672
|
display: "flex",
|
|
@@ -1648,7 +1678,7 @@ var styles = {
|
|
|
1648
1678
|
alignItems: "center",
|
|
1649
1679
|
gap: SPACE.S2,
|
|
1650
1680
|
minHeight: SIZE.control,
|
|
1651
|
-
padding: `${SPACE.
|
|
1681
|
+
padding: `${SPACE.S1} ${SPACE.S3}`,
|
|
1652
1682
|
fontSize: FONT_SIZE.sm,
|
|
1653
1683
|
fontWeight: FONT_WEIGHT.semibold,
|
|
1654
1684
|
color: COLOR.primary,
|
|
@@ -1657,10 +1687,25 @@ var styles = {
|
|
|
1657
1687
|
borderRadius: RADIUS.lg,
|
|
1658
1688
|
cursor: "pointer"
|
|
1659
1689
|
},
|
|
1660
|
-
//
|
|
1661
|
-
//
|
|
1662
|
-
//
|
|
1663
|
-
|
|
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}`,
|
|
1699
|
+
fontSize: FONT_SIZE.sm,
|
|
1700
|
+
fontWeight: FONT_WEIGHT.semibold,
|
|
1701
|
+
color: COLOR.primary,
|
|
1702
|
+
backgroundColor: COLOR.white,
|
|
1703
|
+
border: `1.5px solid ${COLOR.primary}`,
|
|
1704
|
+
borderRadius: RADIUS.lg,
|
|
1705
|
+
cursor: "pointer"
|
|
1706
|
+
},
|
|
1707
|
+
// Channel-info button: people icon + participant count. Opens the
|
|
1708
|
+
// channel settings overlay.
|
|
1664
1709
|
settingsIconButton: {
|
|
1665
1710
|
minHeight: SIZE.control,
|
|
1666
1711
|
display: "inline-flex",
|
|
@@ -2140,6 +2185,62 @@ function AlertIcon({ size = 14, color = "currentColor" }) {
|
|
|
2140
2185
|
}
|
|
2141
2186
|
);
|
|
2142
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
|
+
}
|
|
2143
2244
|
function Spinner({ size = 16, color = COLOR.neutral500, thickness = 2 }) {
|
|
2144
2245
|
return /* @__PURE__ */ jsx(
|
|
2145
2246
|
"span",
|
|
@@ -2228,12 +2329,11 @@ function MessageBubble({
|
|
|
2228
2329
|
...styles5.authorAvatar,
|
|
2229
2330
|
backgroundColor: roleColor(message.senderRole)
|
|
2230
2331
|
},
|
|
2231
|
-
"aria-
|
|
2232
|
-
children:
|
|
2332
|
+
"aria-label": ROLE_LABELS[message.senderRole] ?? message.senderRole,
|
|
2333
|
+
children: /* @__PURE__ */ jsx(RoleAvatarIcon, { role: message.senderRole, size: 20, color: COLOR.white })
|
|
2233
2334
|
}
|
|
2234
2335
|
),
|
|
2235
|
-
/* @__PURE__ */ jsx("span", { style: styles5.authorName, children: message.senderName })
|
|
2236
|
-
/* @__PURE__ */ jsx(RoleBadge, { role: message.senderRole })
|
|
2336
|
+
/* @__PURE__ */ jsx("span", { style: styles5.authorName, children: message.senderName })
|
|
2237
2337
|
] }),
|
|
2238
2338
|
/* @__PURE__ */ jsxs(
|
|
2239
2339
|
"div",
|
|
@@ -2308,9 +2408,6 @@ function MessageBubble({
|
|
|
2308
2408
|
}
|
|
2309
2409
|
);
|
|
2310
2410
|
}
|
|
2311
|
-
function computeInitials(name) {
|
|
2312
|
-
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
|
|
2313
|
-
}
|
|
2314
2411
|
function roleColor(role) {
|
|
2315
2412
|
switch (role) {
|
|
2316
2413
|
case "radiologist":
|
|
@@ -2395,9 +2492,6 @@ function SystemBubble({ message }) {
|
|
|
2395
2492
|
/* @__PURE__ */ jsx("span", { style: styles5.systemTime, children: formatTime(message.insertedAt) })
|
|
2396
2493
|
] }) });
|
|
2397
2494
|
}
|
|
2398
|
-
function RoleBadge({ role }) {
|
|
2399
|
-
return /* @__PURE__ */ jsx("span", { style: { ...styles5.roleBadge, backgroundColor: roleColor(role) }, children: ROLE_LABELS[role] ?? role });
|
|
2400
|
-
}
|
|
2401
2495
|
function formatTime(iso) {
|
|
2402
2496
|
try {
|
|
2403
2497
|
return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
@@ -2499,16 +2593,6 @@ var styles5 = {
|
|
|
2499
2593
|
color: "#fff4d6",
|
|
2500
2594
|
backgroundColor: "rgba(255, 255, 255, 0.2)"
|
|
2501
2595
|
},
|
|
2502
|
-
roleBadge: {
|
|
2503
|
-
fontSize: "10px",
|
|
2504
|
-
fontWeight: FONT_WEIGHT.bold,
|
|
2505
|
-
padding: "2px 7px",
|
|
2506
|
-
borderRadius: RADIUS.sm,
|
|
2507
|
-
textTransform: "uppercase",
|
|
2508
|
-
letterSpacing: "0.06em",
|
|
2509
|
-
color: COLOR.white,
|
|
2510
|
-
flexShrink: 0
|
|
2511
|
-
},
|
|
2512
2596
|
textBody: {
|
|
2513
2597
|
margin: 0,
|
|
2514
2598
|
fontSize: FONT_SIZE.md,
|
|
@@ -2972,9 +3056,6 @@ var styles6 = {
|
|
|
2972
3056
|
alignItems: "center",
|
|
2973
3057
|
gap: SPACE.S3,
|
|
2974
3058
|
margin: `${SPACE.S4} 0 ${SPACE.S3}`,
|
|
2975
|
-
position: "sticky",
|
|
2976
|
-
top: SPACE.S1,
|
|
2977
|
-
zIndex: Z_INDEX.sticky,
|
|
2978
3059
|
pointerEvents: "none"
|
|
2979
3060
|
},
|
|
2980
3061
|
dayHr: {
|
|
@@ -2989,11 +3070,7 @@ var styles6 = {
|
|
|
2989
3070
|
color: COLOR.neutral500,
|
|
2990
3071
|
textTransform: "uppercase",
|
|
2991
3072
|
letterSpacing: "0.06em",
|
|
2992
|
-
// Subtle pill background so the label is readable when it overlays
|
|
2993
|
-
// bubbles passing under it during sticky scroll. Without this the
|
|
2994
|
-
// hairlines visually run through the text on tinted backgrounds.
|
|
2995
3073
|
padding: `2px ${SPACE.S2}`,
|
|
2996
|
-
backgroundColor: COLOR.white,
|
|
2997
3074
|
borderRadius: RADIUS.sm
|
|
2998
3075
|
},
|
|
2999
3076
|
bubbleSlot: {
|
|
@@ -3291,7 +3368,7 @@ function MessageInput({
|
|
|
3291
3368
|
style: styles8.textarea
|
|
3292
3369
|
}
|
|
3293
3370
|
),
|
|
3294
|
-
/* @__PURE__ */
|
|
3371
|
+
/* @__PURE__ */ jsx(
|
|
3295
3372
|
"button",
|
|
3296
3373
|
{
|
|
3297
3374
|
onClick: handleSendText,
|
|
@@ -3303,10 +3380,7 @@ function MessageInput({
|
|
|
3303
3380
|
opacity: text.trim() ? 1 : 0.5
|
|
3304
3381
|
},
|
|
3305
3382
|
type: "button",
|
|
3306
|
-
children:
|
|
3307
|
-
/* @__PURE__ */ jsx(SendIcon, { size: 20, color: COLOR.white }),
|
|
3308
|
-
/* @__PURE__ */ jsx("span", { style: styles8.sendLabel, children: "Send" })
|
|
3309
|
-
]
|
|
3383
|
+
children: /* @__PURE__ */ jsx(SendIcon, { size: 20, color: COLOR.white })
|
|
3310
3384
|
}
|
|
3311
3385
|
)
|
|
3312
3386
|
] })
|
|
@@ -3404,29 +3478,33 @@ var styles8 = {
|
|
|
3404
3478
|
fontFamily: "inherit",
|
|
3405
3479
|
transition: "background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease"
|
|
3406
3480
|
},
|
|
3407
|
-
//
|
|
3408
|
-
//
|
|
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.
|
|
3409
3484
|
sendButton: {
|
|
3410
3485
|
display: "inline-flex",
|
|
3411
3486
|
alignItems: "center",
|
|
3412
|
-
|
|
3413
|
-
|
|
3487
|
+
justifyContent: "center",
|
|
3488
|
+
width: "48px",
|
|
3414
3489
|
height: "48px",
|
|
3415
|
-
padding: `0 ${SPACE.S4}`,
|
|
3416
3490
|
backgroundColor: COLOR.primary,
|
|
3417
3491
|
color: COLOR.white,
|
|
3418
3492
|
border: "none",
|
|
3419
3493
|
borderRadius: "14px",
|
|
3420
3494
|
cursor: "pointer",
|
|
3421
3495
|
flexShrink: 0,
|
|
3422
|
-
fontFamily: "inherit",
|
|
3423
|
-
fontSize: "15px",
|
|
3424
|
-
fontWeight: FONT_WEIGHT.semibold,
|
|
3425
3496
|
transition: "opacity 120ms ease, background-color 120ms ease"
|
|
3426
|
-
},
|
|
3427
|
-
sendLabel: {
|
|
3428
|
-
lineHeight: 1
|
|
3429
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
|
+
};
|
|
3430
3508
|
function ParticipantsList({
|
|
3431
3509
|
participants,
|
|
3432
3510
|
currentUserId,
|
|
@@ -3436,7 +3514,17 @@ function ParticipantsList({
|
|
|
3436
3514
|
}) {
|
|
3437
3515
|
return /* @__PURE__ */ jsx("div", { className, style: styles9.list, children: participants.map((participant) => /* @__PURE__ */ jsxs("div", { style: styles9.item, children: [
|
|
3438
3516
|
/* @__PURE__ */ jsxs("div", { style: styles9.avatar, children: [
|
|
3439
|
-
participant.avatar ? /* @__PURE__ */ jsx("img", { src: participant.avatar, alt: "", style: styles9.avatarImg }) : /* @__PURE__ */ jsx(
|
|
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
|
+
),
|
|
3440
3528
|
/* @__PURE__ */ jsx(
|
|
3441
3529
|
"span",
|
|
3442
3530
|
{
|
|
@@ -3452,7 +3540,7 @@ function ParticipantsList({
|
|
|
3452
3540
|
participant.userName ?? fallbackName(participant.userRole),
|
|
3453
3541
|
participant.userId === currentUserId && /* @__PURE__ */ jsx("span", { style: styles9.youBadge, children: " (you)" })
|
|
3454
3542
|
] }),
|
|
3455
|
-
/* @__PURE__ */ jsx(
|
|
3543
|
+
/* @__PURE__ */ jsx(RoleBadge, { role: participant.userRole })
|
|
3456
3544
|
] }),
|
|
3457
3545
|
isAdmin && participant.userId !== currentUserId && /* @__PURE__ */ jsx(
|
|
3458
3546
|
"button",
|
|
@@ -3480,7 +3568,7 @@ function fallbackName(role) {
|
|
|
3480
3568
|
return "User";
|
|
3481
3569
|
}
|
|
3482
3570
|
}
|
|
3483
|
-
function
|
|
3571
|
+
function RoleBadge({ role }) {
|
|
3484
3572
|
const colors = {
|
|
3485
3573
|
radiologist: { bg: "#eef2ff", text: "#4f46e5" },
|
|
3486
3574
|
// indigo
|
|
@@ -3522,13 +3610,10 @@ var styles9 = {
|
|
|
3522
3610
|
width: "32px",
|
|
3523
3611
|
height: "32px",
|
|
3524
3612
|
borderRadius: RADIUS.full,
|
|
3525
|
-
backgroundColor: COLOR.neutral200,
|
|
3526
3613
|
display: "flex",
|
|
3527
3614
|
alignItems: "center",
|
|
3528
3615
|
justifyContent: "center",
|
|
3529
|
-
|
|
3530
|
-
fontWeight: FONT_WEIGHT.semibold,
|
|
3531
|
-
color: COLOR.neutral500
|
|
3616
|
+
color: COLOR.white
|
|
3532
3617
|
},
|
|
3533
3618
|
statusDot: {
|
|
3534
3619
|
position: "absolute",
|
|
@@ -4120,6 +4205,7 @@ function CollabPanel({
|
|
|
4120
4205
|
showSeenBy = true,
|
|
4121
4206
|
onBack,
|
|
4122
4207
|
hidePatientName = false,
|
|
4208
|
+
hideOpenCase = false,
|
|
4123
4209
|
onConversationChange,
|
|
4124
4210
|
themeMode = "light",
|
|
4125
4211
|
className,
|
|
@@ -4162,6 +4248,7 @@ function CollabPanel({
|
|
|
4162
4248
|
config.onOpenDicom(patientData.studyId, patientData.storageId);
|
|
4163
4249
|
}
|
|
4164
4250
|
};
|
|
4251
|
+
const handleOpenCase = !hideOpenCase && config.onOpenCase ? () => config.onOpenCase?.(patientData.orderId, patientData.displayOrderId) : void 0;
|
|
4165
4252
|
const handleJumpToMessage = (messageId) => {
|
|
4166
4253
|
messageListRef.current?.scrollToMessage(messageId);
|
|
4167
4254
|
};
|
|
@@ -4237,6 +4324,7 @@ function CollabPanel({
|
|
|
4237
4324
|
patientData,
|
|
4238
4325
|
participants,
|
|
4239
4326
|
onOpenDicom: handleOpenDicom,
|
|
4327
|
+
onOpenCase: handleOpenCase,
|
|
4240
4328
|
onOpenSettings: () => setShowSettings(true),
|
|
4241
4329
|
onBack,
|
|
4242
4330
|
hideName: hidePatientName,
|
|
@@ -4491,14 +4579,16 @@ function CollabPopup({
|
|
|
4491
4579
|
),
|
|
4492
4580
|
/* @__PURE__ */ jsxs("div", { style: styles13.titleCenter, children: [
|
|
4493
4581
|
/* @__PURE__ */ jsx("h2", { id: titleId, style: styles13.patientName, children: patientData.patientName || titleText }),
|
|
4494
|
-
(
|
|
4495
|
-
|
|
4496
|
-
patientData.
|
|
4497
|
-
patientData.
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
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
|
+
})()
|
|
4502
4592
|
] }),
|
|
4503
4593
|
/* @__PURE__ */ jsxs("div", { style: styles13.titleActions, children: [
|
|
4504
4594
|
/* @__PURE__ */ jsx(
|
|
@@ -4587,19 +4677,17 @@ var styles13 = {
|
|
|
4587
4677
|
subRow: {
|
|
4588
4678
|
display: "flex",
|
|
4589
4679
|
alignItems: "center",
|
|
4590
|
-
gap: SPACE.S2,
|
|
4591
4680
|
fontSize: FONT_SIZE.sm,
|
|
4592
4681
|
color: COLOR.neutral500,
|
|
4593
4682
|
overflow: "hidden",
|
|
4594
4683
|
textOverflow: "ellipsis",
|
|
4595
4684
|
whiteSpace: "nowrap"
|
|
4596
4685
|
},
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
fontVariantNumeric: "tabular-nums"
|
|
4686
|
+
metaText: {
|
|
4687
|
+
overflow: "hidden",
|
|
4688
|
+
textOverflow: "ellipsis",
|
|
4689
|
+
whiteSpace: "nowrap",
|
|
4690
|
+
minWidth: 0
|
|
4603
4691
|
},
|
|
4604
4692
|
titleActions: {
|
|
4605
4693
|
display: "flex",
|
|
@@ -5412,7 +5500,7 @@ function ConversationListItem({
|
|
|
5412
5500
|
const studyType = item.patientSnapshot?.studyType;
|
|
5413
5501
|
const patientId = item.patientSnapshot?.patientId;
|
|
5414
5502
|
const showStudyRow = !!(studyType || patientId);
|
|
5415
|
-
const initials =
|
|
5503
|
+
const initials = computeInitials(item.patientSnapshot?.patientName || displayName);
|
|
5416
5504
|
return /* @__PURE__ */ jsxs(
|
|
5417
5505
|
"button",
|
|
5418
5506
|
{
|
|
@@ -5507,7 +5595,7 @@ function previewText(message) {
|
|
|
5507
5595
|
}
|
|
5508
5596
|
}
|
|
5509
5597
|
}
|
|
5510
|
-
function
|
|
5598
|
+
function computeInitials(name) {
|
|
5511
5599
|
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("") || "?";
|
|
5512
5600
|
}
|
|
5513
5601
|
var styles15 = {
|
|
@@ -6321,7 +6409,7 @@ function useUnreadCount() {
|
|
|
6321
6409
|
const [counts, setCounts] = useState({});
|
|
6322
6410
|
useEffect(() => {
|
|
6323
6411
|
socket.onUnreadCountUpdate((serverCounts) => {
|
|
6324
|
-
setCounts(serverCounts);
|
|
6412
|
+
setCounts(serverCounts.conversation);
|
|
6325
6413
|
});
|
|
6326
6414
|
}, [socket]);
|
|
6327
6415
|
const getCountForConversation = useCallback(
|