@natoe/colab 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import React4, { createContext, forwardRef, useRef, useImperativeHandle, useEffect, useCallback, useState, useContext, useMemo } from 'react';
1
+ import React4, { createContext, forwardRef, useRef, useImperativeHandle, useEffect, useCallback, useState, useContext, useId, useMemo } from 'react';
2
2
  import { Socket, Presence } from 'phoenix';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -1373,91 +1373,247 @@ var styles3 = {
1373
1373
  textDecoration: "underline"
1374
1374
  }
1375
1375
  };
1376
+ function ReplyIcon({ size = 18, color = "currentColor" }) {
1377
+ return /* @__PURE__ */ jsx(
1378
+ "svg",
1379
+ {
1380
+ xmlns: "http://www.w3.org/2000/svg",
1381
+ width: size,
1382
+ height: size,
1383
+ viewBox: "0 0 24 24",
1384
+ fill: color,
1385
+ "aria-hidden": "true",
1386
+ children: /* @__PURE__ */ jsx("path", { d: "M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z" })
1387
+ }
1388
+ );
1389
+ }
1390
+ function PinIcon({ size = 14, color = "currentColor" }) {
1391
+ return /* @__PURE__ */ jsx(
1392
+ "svg",
1393
+ {
1394
+ xmlns: "http://www.w3.org/2000/svg",
1395
+ width: size,
1396
+ height: size,
1397
+ viewBox: "0 0 24 24",
1398
+ fill: color,
1399
+ "aria-hidden": "true",
1400
+ children: /* @__PURE__ */ jsx("path", { d: "M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z" })
1401
+ }
1402
+ );
1403
+ }
1404
+ function CopyIcon({ size = 18, color = "currentColor" }) {
1405
+ return /* @__PURE__ */ jsx(
1406
+ "svg",
1407
+ {
1408
+ xmlns: "http://www.w3.org/2000/svg",
1409
+ width: size,
1410
+ height: size,
1411
+ viewBox: "0 0 24 24",
1412
+ fill: color,
1413
+ "aria-hidden": "true",
1414
+ children: /* @__PURE__ */ jsx("path", { d: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" })
1415
+ }
1416
+ );
1417
+ }
1418
+ function MoreIcon({ size = 18, color = "currentColor" }) {
1419
+ return /* @__PURE__ */ jsxs(
1420
+ "svg",
1421
+ {
1422
+ xmlns: "http://www.w3.org/2000/svg",
1423
+ width: size,
1424
+ height: size,
1425
+ viewBox: "0 0 24 24",
1426
+ fill: color,
1427
+ "aria-hidden": "true",
1428
+ children: [
1429
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "2" }),
1430
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2" }),
1431
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "19", r: "2" })
1432
+ ]
1433
+ }
1434
+ );
1435
+ }
1376
1436
  function MessageActionsMenu({
1377
1437
  message,
1378
1438
  onReply,
1379
1439
  onPin,
1380
1440
  onUnpin,
1381
1441
  onCopy,
1382
- visible,
1383
1442
  pinDisabled = false,
1443
+ anchorRight = false,
1384
1444
  className
1385
1445
  }) {
1386
- if (!visible) return null;
1446
+ const [isOpen, setIsOpen] = useState(false);
1447
+ const wrapperRef = useRef(null);
1448
+ const triggerRef = useRef(null);
1387
1449
  const isPinned = !!message.isPinned;
1388
1450
  const canCopy = message.type === "text" && !!message.body;
1389
- const handleCopy = () => {
1390
- if (!onCopy) return;
1391
- onCopy(message.body);
1451
+ useEffect(() => {
1452
+ if (!isOpen) return;
1453
+ const handleClick = (e) => {
1454
+ if (!wrapperRef.current?.contains(e.target)) {
1455
+ setIsOpen(false);
1456
+ }
1457
+ };
1458
+ const handleKey = (e) => {
1459
+ if (e.key === "Escape") {
1460
+ setIsOpen(false);
1461
+ triggerRef.current?.focus();
1462
+ }
1463
+ };
1464
+ document.addEventListener("mousedown", handleClick);
1465
+ document.addEventListener("keydown", handleKey);
1466
+ return () => {
1467
+ document.removeEventListener("mousedown", handleClick);
1468
+ document.removeEventListener("keydown", handleKey);
1469
+ };
1470
+ }, [isOpen]);
1471
+ const run = (fn) => {
1472
+ setIsOpen(false);
1473
+ fn();
1392
1474
  };
1393
- return /* @__PURE__ */ jsxs("div", { className, style: styles4.container, children: [
1475
+ return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className, style: styles4.wrapper, children: [
1394
1476
  /* @__PURE__ */ jsx(
1395
1477
  "button",
1396
1478
  {
1397
- onClick: () => onReply(message),
1398
- style: styles4.button,
1399
- title: "Reply",
1479
+ ref: triggerRef,
1400
1480
  type: "button",
1401
- children: "\u21A9"
1481
+ onClick: (e) => {
1482
+ e.stopPropagation();
1483
+ setIsOpen((v) => !v);
1484
+ },
1485
+ "aria-label": "Message actions",
1486
+ "aria-haspopup": "menu",
1487
+ "aria-expanded": isOpen ? "true" : "false",
1488
+ title: "Message actions",
1489
+ style: styles4.trigger,
1490
+ children: /* @__PURE__ */ jsx(MoreIcon, { size: 18, color: "#4b5563" })
1402
1491
  }
1403
1492
  ),
1404
- isPinned ? /* @__PURE__ */ jsx(
1405
- "button",
1406
- {
1407
- onClick: () => onUnpin(message.id),
1408
- style: styles4.button,
1409
- title: "Unpin",
1410
- type: "button",
1411
- children: "\u{1F4CC}"
1412
- }
1413
- ) : /* @__PURE__ */ jsx(
1414
- "button",
1493
+ isOpen && /* @__PURE__ */ jsxs(
1494
+ "div",
1415
1495
  {
1416
- onClick: () => onPin(message.id),
1496
+ role: "menu",
1497
+ "aria-label": "Message actions",
1417
1498
  style: {
1418
- ...styles4.button,
1419
- opacity: pinDisabled ? 0.4 : 1,
1420
- cursor: pinDisabled ? "not-allowed" : "pointer"
1499
+ ...styles4.menu,
1500
+ ...anchorRight ? styles4.menuRight : styles4.menuLeft
1421
1501
  },
1422
- disabled: pinDisabled,
1423
- title: pinDisabled ? "Pin limit reached (max 3)" : "Pin",
1424
- type: "button",
1425
- children: "\u{1F4CC}"
1426
- }
1427
- ),
1428
- canCopy && onCopy && /* @__PURE__ */ jsx(
1429
- "button",
1430
- {
1431
- onClick: handleCopy,
1432
- style: styles4.button,
1433
- title: "Copy text",
1434
- type: "button",
1435
- children: "\u2398"
1502
+ children: [
1503
+ /* @__PURE__ */ jsxs(
1504
+ "button",
1505
+ {
1506
+ type: "button",
1507
+ role: "menuitem",
1508
+ onClick: () => run(() => onReply(message)),
1509
+ style: styles4.item,
1510
+ children: [
1511
+ /* @__PURE__ */ jsx(ReplyIcon, { size: 18, color: "#374151" }),
1512
+ /* @__PURE__ */ jsx("span", { children: "Reply" })
1513
+ ]
1514
+ }
1515
+ ),
1516
+ isPinned ? /* @__PURE__ */ jsxs(
1517
+ "button",
1518
+ {
1519
+ type: "button",
1520
+ role: "menuitem",
1521
+ onClick: () => run(() => onUnpin(message.id)),
1522
+ style: styles4.item,
1523
+ children: [
1524
+ /* @__PURE__ */ jsx(PinIcon, { size: 16, color: "#374151" }),
1525
+ /* @__PURE__ */ jsx("span", { children: "Unpin" })
1526
+ ]
1527
+ }
1528
+ ) : /* @__PURE__ */ jsxs(
1529
+ "button",
1530
+ {
1531
+ type: "button",
1532
+ role: "menuitem",
1533
+ onClick: () => run(() => onPin(message.id)),
1534
+ disabled: pinDisabled,
1535
+ style: {
1536
+ ...styles4.item,
1537
+ ...pinDisabled ? styles4.itemDisabled : {}
1538
+ },
1539
+ title: pinDisabled ? "Pin limit reached (max 3)" : void 0,
1540
+ children: [
1541
+ /* @__PURE__ */ jsx(PinIcon, { size: 16, color: "#374151" }),
1542
+ /* @__PURE__ */ jsx("span", { children: pinDisabled ? "Pin (limit reached)" : "Pin" })
1543
+ ]
1544
+ }
1545
+ ),
1546
+ canCopy && onCopy && /* @__PURE__ */ jsxs(
1547
+ "button",
1548
+ {
1549
+ type: "button",
1550
+ role: "menuitem",
1551
+ onClick: () => run(() => onCopy(message.body)),
1552
+ style: styles4.item,
1553
+ children: [
1554
+ /* @__PURE__ */ jsx(CopyIcon, { size: 16, color: "#374151" }),
1555
+ /* @__PURE__ */ jsx("span", { children: "Copy text" })
1556
+ ]
1557
+ }
1558
+ )
1559
+ ]
1436
1560
  }
1437
1561
  )
1438
1562
  ] });
1439
1563
  }
1440
1564
  var styles4 = {
1441
- container: {
1565
+ wrapper: {
1566
+ position: "relative",
1567
+ display: "inline-block"
1568
+ },
1569
+ trigger: {
1570
+ width: "36px",
1571
+ height: "36px",
1442
1572
  display: "flex",
1443
- gap: "2px",
1444
- padding: "2px",
1573
+ alignItems: "center",
1574
+ justifyContent: "center",
1575
+ backgroundColor: "transparent",
1576
+ border: "none",
1577
+ borderRadius: "50%",
1578
+ cursor: "pointer",
1579
+ color: "#4b5563"
1580
+ },
1581
+ menu: {
1582
+ position: "absolute",
1583
+ top: "100%",
1584
+ marginTop: "4px",
1585
+ minWidth: "160px",
1445
1586
  backgroundColor: "#ffffff",
1446
1587
  border: "1px solid #e5e7eb",
1447
- borderRadius: "20px",
1448
- boxShadow: "0 2px 8px rgba(0, 0, 0, 0.08)"
1588
+ borderRadius: "10px",
1589
+ boxShadow: "0 6px 18px rgba(0, 0, 0, 0.12)",
1590
+ padding: "4px",
1591
+ zIndex: 20
1449
1592
  },
1450
- button: {
1451
- width: "24px",
1452
- height: "24px",
1593
+ menuLeft: {
1594
+ left: 0
1595
+ },
1596
+ menuRight: {
1597
+ right: 0
1598
+ },
1599
+ item: {
1453
1600
  display: "flex",
1454
1601
  alignItems: "center",
1455
- justifyContent: "center",
1456
- fontSize: "12px",
1602
+ gap: "10px",
1603
+ width: "100%",
1604
+ minHeight: "40px",
1605
+ padding: "8px 12px",
1606
+ fontSize: "14px",
1607
+ color: "#111827",
1457
1608
  backgroundColor: "transparent",
1458
1609
  border: "none",
1459
- borderRadius: "50%",
1460
- cursor: "pointer"
1610
+ borderRadius: "6px",
1611
+ cursor: "pointer",
1612
+ textAlign: "left"
1613
+ },
1614
+ itemDisabled: {
1615
+ color: "#9ca3af",
1616
+ cursor: "not-allowed"
1461
1617
  }
1462
1618
  };
1463
1619
  function FileIcon({ size = 18, color = "currentColor" }) {
@@ -1474,20 +1630,6 @@ function FileIcon({ size = 18, color = "currentColor" }) {
1474
1630
  }
1475
1631
  );
1476
1632
  }
1477
- function PinIcon({ size = 14, color = "currentColor" }) {
1478
- return /* @__PURE__ */ jsx(
1479
- "svg",
1480
- {
1481
- xmlns: "http://www.w3.org/2000/svg",
1482
- width: size,
1483
- height: size,
1484
- viewBox: "0 0 24 24",
1485
- fill: color,
1486
- "aria-hidden": "true",
1487
- children: /* @__PURE__ */ jsx("path", { d: "M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z" })
1488
- }
1489
- );
1490
- }
1491
1633
  function AlertIcon({ size = 14, color = "currentColor" }) {
1492
1634
  return /* @__PURE__ */ jsx(
1493
1635
  "svg",
@@ -1552,12 +1694,26 @@ function MessageBubble({
1552
1694
  pinDisabled = false,
1553
1695
  className
1554
1696
  }) {
1555
- const [isHovered, setIsHovered] = useState(false);
1556
1697
  if (message.type === "system") {
1557
1698
  return /* @__PURE__ */ jsx(SystemBubble, { message });
1558
1699
  }
1559
1700
  const hasActions = !!(onReply || onPin || onUnpin || onCopy);
1560
- return /* @__PURE__ */ jsx(
1701
+ const actionsSlot = hasActions ? /* @__PURE__ */ jsx("div", { style: styles5.actionsSlot, children: /* @__PURE__ */ jsx(
1702
+ MessageActionsMenu,
1703
+ {
1704
+ message,
1705
+ onReply: onReply ?? (() => {
1706
+ }),
1707
+ onPin: onPin ?? (() => {
1708
+ }),
1709
+ onUnpin: onUnpin ?? (() => {
1710
+ }),
1711
+ onCopy,
1712
+ pinDisabled,
1713
+ anchorRight: !isOwn
1714
+ }
1715
+ ) }) : null;
1716
+ return /* @__PURE__ */ jsxs(
1561
1717
  "div",
1562
1718
  {
1563
1719
  className,
@@ -1565,10 +1721,9 @@ function MessageBubble({
1565
1721
  ...styles5.wrapper,
1566
1722
  justifyContent: isOwn ? "flex-end" : "flex-start"
1567
1723
  },
1568
- onMouseEnter: () => setIsHovered(true),
1569
- onMouseLeave: () => setIsHovered(false),
1570
- children: /* @__PURE__ */ jsxs("div", { style: styles5.bubbleWrapper, children: [
1571
- /* @__PURE__ */ jsxs(
1724
+ children: [
1725
+ isOwn && actionsSlot,
1726
+ /* @__PURE__ */ jsx("div", { style: styles5.bubbleWrapper, children: /* @__PURE__ */ jsxs(
1572
1727
  "div",
1573
1728
  {
1574
1729
  style: {
@@ -1627,33 +1782,9 @@ function MessageBubble({
1627
1782
  )
1628
1783
  ]
1629
1784
  }
1630
- ),
1631
- hasActions && /* @__PURE__ */ jsx(
1632
- "div",
1633
- {
1634
- style: {
1635
- ...styles5.actionsWrapper,
1636
- [isOwn ? "right" : "left"]: "100%",
1637
- [isOwn ? "marginRight" : "marginLeft"]: "6px"
1638
- },
1639
- children: /* @__PURE__ */ jsx(
1640
- MessageActionsMenu,
1641
- {
1642
- message,
1643
- onReply: onReply ?? (() => {
1644
- }),
1645
- onPin: onPin ?? (() => {
1646
- }),
1647
- onUnpin: onUnpin ?? (() => {
1648
- }),
1649
- onCopy,
1650
- visible: isHovered,
1651
- pinDisabled
1652
- }
1653
- )
1654
- }
1655
- )
1656
- ] })
1785
+ ) }),
1786
+ !isOwn && actionsSlot
1787
+ ]
1657
1788
  }
1658
1789
  );
1659
1790
  }
@@ -1750,6 +1881,8 @@ function escapeRegex2(string) {
1750
1881
  var styles5 = {
1751
1882
  wrapper: {
1752
1883
  display: "flex",
1884
+ alignItems: "flex-end",
1885
+ gap: "4px",
1753
1886
  marginBottom: "8px",
1754
1887
  paddingLeft: "16px",
1755
1888
  paddingRight: "16px"
@@ -1758,6 +1891,10 @@ var styles5 = {
1758
1891
  position: "relative",
1759
1892
  maxWidth: "75%"
1760
1893
  },
1894
+ actionsSlot: {
1895
+ flexShrink: 0,
1896
+ alignSelf: "flex-end"
1897
+ },
1761
1898
  bubble: {
1762
1899
  padding: "10px 14px",
1763
1900
  borderRadius: "12px",
@@ -1833,12 +1970,6 @@ var styles5 = {
1833
1970
  cursor: "pointer",
1834
1971
  padding: "0 2px"
1835
1972
  },
1836
- actionsWrapper: {
1837
- position: "absolute",
1838
- top: "50%",
1839
- transform: "translateY(-50%)",
1840
- zIndex: 10
1841
- },
1842
1973
  audioContainer: {
1843
1974
  display: "flex",
1844
1975
  alignItems: "center",
@@ -2488,7 +2619,18 @@ function MessageInput({
2488
2619
  type: "file",
2489
2620
  onChange: handleFileSelect,
2490
2621
  style: { display: "none" },
2491
- accept: [...SUPPORTED_IMAGE_TYPES, "application/pdf", ".doc", ".docx"].join(",")
2622
+ accept: [
2623
+ ...SUPPORTED_IMAGE_TYPES,
2624
+ "application/pdf",
2625
+ ".doc",
2626
+ ".docx",
2627
+ ".xls",
2628
+ ".xlsx",
2629
+ ".csv",
2630
+ ".txt",
2631
+ ".heic",
2632
+ ".heif"
2633
+ ].join(",")
2492
2634
  }
2493
2635
  ),
2494
2636
  /* @__PURE__ */ jsx(
@@ -3500,6 +3642,7 @@ function ExpandIcon({ size = 18, color = "currentColor" }) {
3500
3642
  }
3501
3643
  );
3502
3644
  }
3645
+ var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3503
3646
  function CollabPopup({
3504
3647
  orderId,
3505
3648
  patientData,
@@ -3521,6 +3664,8 @@ function CollabPopup({
3521
3664
  const [isMinimized, setIsMinimized] = useState(false);
3522
3665
  const dragOffset = useRef({ x: 0, y: 0 });
3523
3666
  const containerRef = useRef(null);
3667
+ const previouslyFocused = useRef(null);
3668
+ const titleId = useId();
3524
3669
  const handleMouseDown = useCallback(
3525
3670
  (e) => {
3526
3671
  if (!e.target.closest("[data-drag-handle]")) return;
@@ -3535,13 +3680,44 @@ function CollabPopup({
3535
3680
  );
3536
3681
  useEffect(() => {
3537
3682
  if (!isOpen) return;
3683
+ previouslyFocused.current = document.activeElement;
3684
+ const focusFirst = () => {
3685
+ const root = containerRef.current;
3686
+ if (!root) return;
3687
+ const first = root.querySelector(FOCUSABLE_SELECTOR);
3688
+ (first ?? root).focus();
3689
+ };
3690
+ const raf = requestAnimationFrame(focusFirst);
3538
3691
  const handleKey = (e) => {
3539
3692
  if (e.key === "Escape") {
3540
3693
  onClose();
3694
+ return;
3695
+ }
3696
+ if (e.key !== "Tab") return;
3697
+ const root = containerRef.current;
3698
+ if (!root) return;
3699
+ const focusables = Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR));
3700
+ if (focusables.length === 0) {
3701
+ e.preventDefault();
3702
+ return;
3703
+ }
3704
+ const first = focusables[0];
3705
+ const last = focusables[focusables.length - 1];
3706
+ const active = document.activeElement;
3707
+ if (e.shiftKey && active === first) {
3708
+ e.preventDefault();
3709
+ last.focus();
3710
+ } else if (!e.shiftKey && active === last) {
3711
+ e.preventDefault();
3712
+ first.focus();
3541
3713
  }
3542
3714
  };
3543
3715
  window.addEventListener("keydown", handleKey);
3544
- return () => window.removeEventListener("keydown", handleKey);
3716
+ return () => {
3717
+ cancelAnimationFrame(raf);
3718
+ window.removeEventListener("keydown", handleKey);
3719
+ previouslyFocused.current?.focus?.();
3720
+ };
3545
3721
  }, [isOpen, onClose]);
3546
3722
  useEffect(() => {
3547
3723
  if (!isDragging) return;
@@ -3567,6 +3743,10 @@ function CollabPopup({
3567
3743
  {
3568
3744
  ref: containerRef,
3569
3745
  className,
3746
+ role: "dialog",
3747
+ "aria-modal": "true",
3748
+ "aria-labelledby": titleId,
3749
+ tabIndex: -1,
3570
3750
  style: {
3571
3751
  ...styles13.container,
3572
3752
  left: position.x,
@@ -3578,7 +3758,7 @@ function CollabPopup({
3578
3758
  onMouseDown: handleMouseDown,
3579
3759
  children: [
3580
3760
  /* @__PURE__ */ jsxs("div", { "data-drag-handle": true, style: styles13.titleBar, children: [
3581
- /* @__PURE__ */ jsx("span", { style: styles13.titleText, children: buildChannelName(patientData) }),
3761
+ /* @__PURE__ */ jsx("span", { id: titleId, style: styles13.titleText, children: buildChannelName(patientData) }),
3582
3762
  /* @__PURE__ */ jsxs("div", { style: styles13.titleActions, children: [
3583
3763
  /* @__PURE__ */ jsx(
3584
3764
  "button",