@auxilium/datalynk-client 1.6.2 → 1.6.4

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/chat.mjs CHANGED
@@ -1390,6 +1390,9 @@ class Chat {
1390
1390
  __publicField(this, "socketUnsubscribe");
1391
1391
  __publicField(this, "typingTimers", /* @__PURE__ */ new Map());
1392
1392
  __publicField(this, "initialized", false);
1393
+ __publicField(this, "resyncing");
1394
+ __publicField(this, "readVisibility", () => false);
1395
+ __publicField(this, "directorySeq", 0);
1393
1396
  this.api = api;
1394
1397
  }
1395
1398
  async initialize(force = false) {
@@ -1415,14 +1418,12 @@ class Chat {
1415
1418
  }
1416
1419
  async loadDirectory(search = "", cursor) {
1417
1420
  const query = search.trim();
1418
- if (query.length < 2) {
1419
- this.users$.next([]);
1420
- return [];
1421
- }
1421
+ const seq = ++this.directorySeq;
1422
1422
  const response = await this.run({
1423
- "$/chat/directory": { query, cursor, limit: 25 }
1423
+ "$/chat/directory": { query, cursor, limit: 10 }
1424
1424
  });
1425
1425
  const users = response.users || [];
1426
+ if (seq !== this.directorySeq) return users;
1426
1427
  this.users$.next(users);
1427
1428
  const selected = this.selectedUser$.value;
1428
1429
  const replacement = selected && users.find((user) => user.id === selected.id);
@@ -1452,7 +1453,8 @@ class Chat {
1452
1453
  "$/chat/history": { userId, before, limit: 75 }
1453
1454
  });
1454
1455
  const messages = (response.messages || []).map((message) => ({ ...message, status: "sent" }));
1455
- this.messages$.next(before ? this.mergeMessages(messages, this.messages$.value) : messages);
1456
+ if (this.selectedUser$.value && this.selectedUser$.value.id !== userId) return messages;
1457
+ this.messages$.next(this.sortMessages(this.mergeMessages(messages, this.messages$.value)));
1456
1458
  this.hasMore$.next(Boolean(response.hasMore));
1457
1459
  return messages;
1458
1460
  } finally {
@@ -1495,7 +1497,15 @@ class Chat {
1495
1497
  async setTyping(userId, typing) {
1496
1498
  await this.run({ "$/chat/typing": { userId, typing } }, true);
1497
1499
  }
1500
+ /** Only the visible chat window may acknowledge messages as seen. */
1501
+ setReadVisibility(visible) {
1502
+ this.readVisibility = visible;
1503
+ return () => {
1504
+ if (this.readVisibility === visible) this.readVisibility = () => false;
1505
+ };
1506
+ }
1498
1507
  async markLatestRead() {
1508
+ if (!this.readVisibility()) return;
1499
1509
  const peer = this.selectedUser$.value;
1500
1510
  const messages = this.messages$.value;
1501
1511
  const latest = [...messages].reverse().find((message) => message.senderId === (peer == null ? void 0 : peer.id) && !message.id.startsWith("pending:"));
@@ -1520,17 +1530,43 @@ class Chat {
1520
1530
  () => this.api.socket.send({ chat: { subscribe: true } })
1521
1531
  );
1522
1532
  }
1533
+ /**
1534
+ * Catch up after every (re)subscribe. Events published while the socket was down
1535
+ * (up to the reconnect delay plus ticket fetch) are never replayed, so reload the
1536
+ * inbox and the newest page of the open thread from the API.
1537
+ */
1538
+ resync() {
1539
+ if (this.resyncing) return this.resyncing;
1540
+ this.resyncing = (async () => {
1541
+ var _a;
1542
+ try {
1543
+ await this.loadConversations();
1544
+ const peer = this.selectedUser$.value;
1545
+ if (!peer) return;
1546
+ const response = await this.run({ "$/chat/history": { userId: peer.id, limit: 75 } }, true);
1547
+ if (((_a = this.selectedUser$.value) == null ? void 0 : _a.id) !== peer.id) return;
1548
+ const latest = (response.messages || []).map((message) => ({ ...message, status: "sent" }));
1549
+ this.messages$.next(this.sortMessages(this.mergeMessages(latest, this.messages$.value)));
1550
+ await this.markLatestRead();
1551
+ } catch {
1552
+ } finally {
1553
+ this.resyncing = void 0;
1554
+ }
1555
+ })();
1556
+ return this.resyncing;
1557
+ }
1523
1558
  onSocketEvent(event) {
1524
1559
  var _a, _b, _c, _d, _e;
1525
1560
  if (!(event == null ? void 0 : event.type) || event.type.indexOf("chat.") !== 0) return;
1526
- if (event.type === "chat.message") {
1561
+ if (event.type === "chat.subscribed") {
1562
+ void this.resync();
1563
+ } else if (event.type === "chat.message") {
1527
1564
  const message = (_a = event.data) == null ? void 0 : _a.message;
1528
1565
  const selected = this.selectedUser$.value;
1529
1566
  if (message && selected && (message.senderId === selected.id || message.recipientId === selected.id)) {
1530
1567
  this.upsertMessage({ ...message, status: "sent" });
1531
- if (message.senderId === selected.id) void this.markLatestRead();
1532
1568
  }
1533
- void this.loadConversations();
1569
+ void this.loadConversations().then(() => this.markLatestRead()).catch(() => void 0);
1534
1570
  } else if (event.type === "chat.read") {
1535
1571
  this.applyReadReceipt(event.data);
1536
1572
  void this.loadConversations();
@@ -1584,6 +1620,12 @@ class Chat {
1584
1620
  this.messages$.next(next);
1585
1621
  }
1586
1622
  }
1623
+ /** Stream order for persisted messages; optimistic/failed sends stay at the end in send order. */
1624
+ sortMessages(messages) {
1625
+ const persisted = messages.filter((message) => !message.id.startsWith("pending:"));
1626
+ const pending = messages.filter((message) => message.id.startsWith("pending:"));
1627
+ return [...persisted.sort((a, b) => this.compareStreamIds(a.id, b.id)), ...pending];
1628
+ }
1587
1629
  mergeMessages(first, second) {
1588
1630
  const out = [];
1589
1631
  const seen = /* @__PURE__ */ new Set();
@@ -1622,16 +1664,23 @@ class Chat {
1622
1664
  }
1623
1665
  }
1624
1666
  const icon = (path) => `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${path}"/></svg>`;
1667
+ const SOUND_ON_ICON = "M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05A4.47 4.47 0 0 0 16.5 12zM14 3.23v2.06a7 7 0 0 1 0 13.42v2.06A9 9 0 0 0 14 3.23z";
1668
+ const SOUND_OFF_ICON = "M16.5 12A4.5 4.5 0 0 0 14 7.97v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.8 8.8 0 0 0 21 12a9 9 0 0 0-7-8.77v2.06A7 7 0 0 1 19 12zM4.27 3 3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06a9 9 0 0 0 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4 9.91 6.09 12 8.18V4z";
1625
1669
  const CHAT_HTML = `
1626
1670
  <section class="dl-chat" role="dialog" aria-label="Datalynk Chat">
1627
1671
  <header class="dl-chat__titlebar">
1628
- <div class="dl-chat__brand"><span class="dl-chat__mark">${icon("M4 5h16v11H9l-5 4V5zm3 4v2h2V9H7zm4 0v2h2V9h-2zm4 0v2h2V9h-2z")}</span><strong>Chat</strong></div>
1672
+ <div class="dl-chat__brand"><span class="dl-chat__mark">${icon("M4 5h16v11H9l-5 4V5zm3 4v2h2V9H7zm4 0v2h2V9h-2zm4 0v2h2V9h-2z")}</span><strong>Chat</strong><span class="dl-chat__title-unread" aria-live="polite" hidden></span></div>
1629
1673
  <div class="dl-chat__window-actions">
1674
+ <button class="dl-chat__icon-button dl-chat__sound" type="button" aria-pressed="false" aria-label="Mute message sound" title="Mute message sound">${icon(SOUND_ON_ICON)}</button>
1630
1675
  <button class="dl-chat__icon-button dl-chat__popout" type="button" aria-label="Open chat in a separate window" title="Open in new window">${icon("M14 3h7v7h-2V6.41l-9.29 9.3-1.42-1.42 9.3-9.29H14V3zM5 5h6v2H5v12h12v-6h2v8H3V5h2z")}</button>
1631
1676
  <button class="dl-chat__icon-button dl-chat__minimize" type="button" aria-label="Minimize chat" title="Minimize">${icon("M5 11h14v2H5z")}</button>
1632
1677
  <button class="dl-chat__icon-button dl-chat__close" type="button" aria-label="Close chat" title="Close">${icon("M6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19 5 17.6 10.6 12 5 6.4z")}</button>
1633
1678
  </div>
1634
1679
  </header>
1680
+ <button class="dl-chat__restore" type="button" aria-label="Restore chat" hidden>
1681
+ <span class="dl-chat__mark">${icon("M4 5h16v11H9l-5 4V5zm3 4v2h2V9H7zm4 0v2h2V9h-2zm4 0v2h2V9h-2z")}</span>
1682
+ <span><strong>Chat</strong><small>Open conversations</small></span>
1683
+ </button>
1635
1684
  <div class="dl-chat__connection" role="status" hidden></div>
1636
1685
  <div class="dl-chat__error" role="alert" hidden></div>
1637
1686
  <div class="dl-chat__body">
@@ -1677,25 +1726,39 @@ const CHAT_CSS = `
1677
1726
  :host{all:initial;position:fixed;inset:0;z-index:2147483000;pointer-events:none;color:#17242b;font-family:"Segoe UI",Tahoma,Arial,sans-serif}
1678
1727
  *{box-sizing:border-box}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
1679
1728
  button,input,textarea{font:inherit}
1680
- .dl-chat{--teal:#3f6f85;--teal-deep:#2f596d;--teal-soft:#e9f1f4;--red:#c42d27;--green:#599e21;--ink:#17242b;--muted:#60717a;--line:#d7e0e4;position:fixed;right:22px;bottom:22px;width:min(720px,calc(100vw - 32px));height:min(640px,calc(100dvh - 32px));min-width:360px;min-height:460px;display:flex;flex-direction:column;overflow:hidden;resize:both;pointer-events:auto;background:#fff;border:1px solid #c7d2d7;border-radius:10px;box-shadow:0 18px 54px rgba(23,36,43,.24),0 3px 10px rgba(23,36,43,.12);container-type:inline-size}
1729
+ .dl-chat{--teal:var(--primary,#3f6f85);--teal-fore:var(--primary-fore,#fff);--teal-deep:color-mix(in srgb,var(--teal) 78%,#000);--teal-soft:color-mix(in srgb,var(--teal) 12%,#fff);--teal-wash:color-mix(in srgb,var(--teal) 5%,#fff);--teal-hover:color-mix(in srgb,var(--teal) 10%,#fff);--teal-selected:color-mix(in srgb,var(--teal) 17%,#fff);--red:var(--accent,#c42d27);--red-fore:var(--accentText,#fff);--green:#599e21;--ink:#17242b;--muted:#60717a;--line:#d7e0e4;position:fixed;right:22px;bottom:22px;width:min(720px,calc(100vw - 32px));height:min(640px,calc(100dvh - 32px));min-width:360px;min-height:460px;display:flex;flex-direction:column;overflow:hidden;resize:both;pointer-events:auto;background:#fff;border:1px solid #c7d2d7;border-radius:10px;box-shadow:0 18px 54px rgba(23,36,43,.24),0 3px 10px rgba(23,36,43,.12);container-type:inline-size}
1681
1730
  .dl-chat.is-popout{inset:0;width:100%;height:100dvh;min-width:0;min-height:0;resize:none;border:0;border-radius:0;box-shadow:none}.dl-chat.is-popout .dl-chat__minimize{display:none}
1682
- .dl-chat__titlebar{height:46px;flex:0 0 46px;display:flex;align-items:center;justify-content:space-between;padding:0 8px 0 14px;color:#fff;background:var(--teal);cursor:move;touch-action:none;user-select:none}
1731
+ .dl-chat__titlebar{height:46px;flex:0 0 46px;display:flex;align-items:center;justify-content:space-between;padding:0 8px 0 14px;color:var(--teal-fore);background:var(--teal);cursor:move;touch-action:none;user-select:none}
1683
1732
  .dl-chat__brand{display:flex;align-items:center;gap:9px;font-size:15px;letter-spacing:.01em}.dl-chat__mark{display:grid;place-items:center;width:24px;height:24px}.dl-chat__mark svg{width:22px;height:22px;fill:currentColor}
1733
+ .dl-chat__title-unread{min-width:20px;height:20px;padding:0 6px;border-radius:10px;font-size:12px;font-weight:700;line-height:20px;text-align:center;color:var(--red-fore);background:var(--red);box-shadow:0 0 0 2px rgba(255,255,255,.85)}.dl-chat__title-unread[hidden]{display:none}
1684
1734
  .dl-chat__window-actions{display:flex;gap:2px}.dl-chat__icon-button{display:grid;place-items:center;width:34px;height:34px;padding:0;border:0;border-radius:5px;color:inherit;background:transparent;cursor:pointer;transition:background-color 120ms ease-out,color 120ms ease-out}.dl-chat__icon-button:hover{background:rgba(255,255,255,.14)}.dl-chat__icon-button:focus-visible,.dl-chat button:focus-visible,.dl-chat input:focus-visible,.dl-chat textarea:focus-visible{outline:2px solid #f2c94c;outline-offset:2px}.dl-chat__icon-button svg{width:18px;height:18px;fill:currentColor}
1685
1735
  .dl-chat__connection,.dl-chat__error{padding:7px 14px;font-size:12px;line-height:1.35}.dl-chat__connection{color:#654d00;background:#fff4c7;border-bottom:1px solid #e8d68c}.dl-chat__error{color:#7b201c;background:#fbe9e8;border-bottom:1px solid #efbfbc}
1686
- .dl-chat__body{min-height:0;flex:1;display:grid;grid-template-columns:260px minmax(0,1fr)}.dl-chat__sidebar{min-width:0;display:flex;flex-direction:column;background:#f4f7f8;border-right:1px solid var(--line)}
1687
- .dl-chat__search-wrap{position:relative;display:block;margin:14px 12px 9px}.dl-chat__search-wrap>svg{position:absolute;left:10px;top:50%;width:17px;height:17px;fill:#70818a;transform:translateY(-50%);pointer-events:none}.dl-chat__search{width:100%;height:36px;padding:0 10px 0 34px;color:var(--ink);background:#fff;border:1px solid #c7d2d7;border-radius:6px;outline:0}.dl-chat__search:focus,.dl-chat__composer:focus{border-color:var(--teal);box-shadow:0 0 0 2px rgba(63,111,133,.15)}
1688
- .dl-chat__scroll{min-height:0;overflow:auto;padding:0 8px 16px}.dl-chat__scroll h2{margin:14px 8px 6px;color:#63757e;font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase}
1689
- .dl-chat__person{position:relative;width:100%;display:grid;grid-template-columns:38px minmax(0,1fr) 8px;align-items:center;gap:10px;padding:8px;border:0;border-radius:7px;text-align:left;color:var(--ink);background:transparent;cursor:pointer;transition:background-color 120ms ease-out}.dl-chat__person:hover{background:#e5edef}.dl-chat__person.is-selected{background:#dce8ed}.dl-chat__avatar{display:grid;place-items:center;width:38px;height:38px;border-radius:50%;color:#fff;background:var(--teal);font-size:12px;font-weight:700;letter-spacing:.03em}.dl-chat__person-copy{min-width:0}.dl-chat__person-top,.dl-chat__person-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}.dl-chat__person-top strong,.dl-chat__person-bottom>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-chat__person-top strong{font-size:13px;font-weight:600}.dl-chat__person-top time{flex:0 0 auto;color:#778891;font-size:10px}.dl-chat__person-bottom>span:first-child{color:var(--muted);font-size:11px}.dl-chat__presence{width:8px;height:8px;border:2px solid #87969d;border-radius:50%}.dl-chat__presence.is-online{border-color:var(--green);background:var(--green)}.dl-chat__unread{display:grid;place-items:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;color:#fff!important;background:var(--red);font-size:10px!important;font-weight:700}.dl-chat__list-empty{margin:6px 8px 12px;color:var(--muted);font-size:12px;line-height:1.45}
1690
- .dl-chat__main{min-width:0;min-height:0}.dl-chat__empty{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:36px;text-align:center}.dl-chat__empty-icon{display:grid;place-items:center;width:58px;height:58px;color:var(--teal);background:var(--teal-soft);border-radius:50%}.dl-chat__empty-icon svg{width:27px;height:27px;fill:currentColor}.dl-chat__empty h2{margin:17px 0 5px;font-size:17px}.dl-chat__empty p{max-width:280px;margin:0;color:var(--muted);font-size:13px;line-height:1.5}
1691
- .dl-chat__thread{height:100%;display:grid;grid-template-rows:auto auto minmax(0,1fr) auto auto}.dl-chat__thread[hidden],.dl-chat__empty[hidden]{display:none}.dl-chat__thread-header{height:62px;display:flex;align-items:center;gap:10px;padding:0 16px;border-bottom:1px solid var(--line)}.dl-chat__thread-header .dl-chat__avatar{width:36px;height:36px;flex:0 0 36px}.dl-chat__thread-header>div{min-width:0;display:flex;flex-direction:column}.dl-chat__thread-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.dl-chat__thread-status{position:relative;padding-left:11px;color:var(--muted);font-size:11px}.dl-chat__thread-status:before{content:"";position:absolute;left:0;top:50%;width:7px;height:7px;border:1.5px solid #87969d;border-radius:50%;transform:translateY(-50%)}.dl-chat__thread-status.is-online:before{border-color:var(--green);background:var(--green)}.dl-chat__back{display:none;color:var(--teal)}
1692
- .dl-chat__older{justify-self:center;margin:7px 0 0;padding:4px 8px;border:0;color:var(--teal-deep);background:transparent;font-size:11px;cursor:pointer}.dl-chat__older:hover{text-decoration:underline}.dl-chat__messages{min-height:0;overflow-y:auto;padding:12px 18px 18px;scroll-behavior:smooth}.dl-chat__date{display:flex;align-items:center;gap:10px;margin:12px 0;color:#71838c;font-size:10px}.dl-chat__date:before,.dl-chat__date:after{content:"";height:1px;flex:1;background:#e3e9ec}.dl-chat__message{display:flex;margin:4px 0}.dl-chat__message.is-mine{justify-content:flex-end}.dl-chat__bubble{max-width:min(76%,430px);padding:8px 10px 6px;color:var(--ink);background:#edf2f4;border-radius:5px 11px 11px 11px;font-size:13px;line-height:1.42;overflow-wrap:anywhere}.dl-chat__message.is-mine .dl-chat__bubble{color:#fff;background:var(--teal);border-radius:11px 5px 11px 11px}.dl-chat__bubble>span{white-space:pre-wrap}.dl-chat__bubble time{display:block;margin-top:3px;color:#6c7e87;font-size:9px;text-align:right}.dl-chat__message.is-mine time{color:rgba(255,255,255,.72)}.dl-chat__retry{display:block;margin:5px 0 0 auto;padding:2px 7px;border:1px solid rgba(255,255,255,.65);border-radius:4px;color:#fff;background:transparent;font-size:10px;cursor:pointer}.dl-chat__retry:hover{background:rgba(255,255,255,.12)}.dl-chat__thread-empty{max-width:300px;margin:auto;padding:24px;color:var(--muted);font-size:12px;line-height:1.5;text-align:center}.dl-chat__typing{height:20px;padding:0 18px;color:var(--muted);font-size:11px;font-style:italic}
1693
- .dl-chat__compose{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 38px;gap:8px;padding:10px 14px 22px;border-top:1px solid var(--line)}.dl-chat__composer{width:100%;min-height:38px;max-height:92px;resize:none;padding:9px 11px;color:var(--ink);border:1px solid #bdcbd1;border-radius:7px;line-height:1.35;outline:0}.dl-chat__send{display:grid;place-items:center;width:38px;height:38px;padding:0;border:0;border-radius:7px;color:#fff;background:var(--teal);cursor:pointer;transition:background-color 120ms ease-out}.dl-chat__send:hover{background:var(--teal-deep)}.dl-chat__send:disabled{cursor:not-allowed;opacity:.45}.dl-chat__send svg{width:19px;height:19px;fill:currentColor}.dl-chat__hint{position:absolute;left:15px;bottom:5px;color:#829098;font-size:9px}
1694
- .dl-chat.is-minimized{left:auto!important;top:auto!important;right:22px!important;bottom:0!important;width:280px!important;height:46px!important;min-height:46px;resize:none;border-radius:9px 9px 0 0}.dl-chat.is-minimized .dl-chat__titlebar{cursor:pointer}.dl-chat.is-minimized .dl-chat__body,.dl-chat.is-minimized .dl-chat__connection,.dl-chat.is-minimized .dl-chat__error{display:none}
1695
- @container (max-width:520px){.dl-chat__body{grid-template-columns:1fr}.dl-chat__main{display:none}.dl-chat.has-selection .dl-chat__sidebar{display:none}.dl-chat.has-selection .dl-chat__main{display:block}.dl-chat__back{display:grid}.dl-chat__hint{display:none}.dl-chat__compose{padding-bottom:10px}}
1696
- @media(max-width:600px){.dl-chat{inset:0;width:100%;height:100dvh;min-width:0;min-height:0;border:0;border-radius:0;resize:none}.dl-chat.is-minimized{inset:auto 12px 0 auto!important;width:min(280px,calc(100vw - 24px))!important;border:1px solid #c7d2d7;border-bottom:0}.dl-chat__titlebar{cursor:default}.dl-chat__popout{display:none}}
1736
+ .dl-chat__body{min-height:0;flex:1;display:grid;grid-template-columns:260px minmax(0,1fr);grid-template-rows:minmax(0,1fr)}.dl-chat__sidebar{min-width:0;min-height:0;display:flex;flex-direction:column;background:var(--teal-wash);border-right:1px solid var(--line)}
1737
+ .dl-chat__search-wrap{position:relative;display:block;margin:14px 12px 9px}.dl-chat__search-wrap>svg{position:absolute;left:10px;top:50%;width:17px;height:17px;fill:#70818a;transform:translateY(-50%);pointer-events:none}.dl-chat__search{width:100%;height:36px;padding:0 10px 0 34px;color:var(--ink);background:#fff;border:1px solid #c7d2d7;border-radius:6px;outline:0}.dl-chat__search:focus,.dl-chat__composer:focus{border-color:var(--teal);box-shadow:0 0 0 2px color-mix(in srgb,var(--teal) 15%,transparent)}
1738
+ .dl-chat__scroll{flex:1 1 auto;min-height:0;overflow:auto;padding:0 8px 16px}.dl-chat__scroll h2{margin:14px 8px 6px;color:#63757e;font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase}
1739
+ .dl-chat__person{position:relative;width:100%;display:grid;grid-template-columns:38px minmax(0,1fr) 8px;align-items:center;gap:10px;padding:8px;border:0;border-radius:7px;text-align:left;color:var(--ink);background:transparent;cursor:pointer;transition:background-color 120ms ease-out}.dl-chat__person:hover{background:var(--teal-hover)}.dl-chat__person.is-selected{background:var(--teal-selected)}.dl-chat__avatar{display:grid;place-items:center;width:38px;height:38px;border-radius:50%;color:var(--teal-fore);background:var(--teal);font-size:12px;font-weight:700;letter-spacing:.03em}.dl-chat__person-copy{min-width:0}.dl-chat__person-top,.dl-chat__person-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}.dl-chat__person-top strong,.dl-chat__person-bottom>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-chat__person-top strong{font-size:13px;font-weight:600}.dl-chat__person-top time{flex:0 0 auto;color:#778891;font-size:10px}.dl-chat__person-bottom>span:first-child{color:var(--muted);font-size:11px}.dl-chat__presence{width:8px;height:8px;border:2px solid #87969d;border-radius:50%}.dl-chat__presence.is-online{border-color:var(--green);background:var(--green)}.dl-chat__unread{display:grid;place-items:center;min-width:18px;height:18px;padding:0 5px;border-radius:9px;color:var(--red-fore)!important;background:var(--red);font-size:10px!important;font-weight:700}.dl-chat__list-empty{margin:6px 8px 12px;color:var(--muted);font-size:12px;line-height:1.45}
1740
+ .dl-chat__main{min-width:0;min-height:0;overflow:hidden;height:100%;display:flex;flex-direction:column}.dl-chat__empty{flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:36px;text-align:center}.dl-chat__empty-icon{display:grid;place-items:center;width:58px;height:58px;color:var(--teal);background:var(--teal-soft);border-radius:50%}.dl-chat__empty-icon svg{width:27px;height:27px;fill:currentColor}.dl-chat__empty h2{margin:17px 0 5px;font-size:17px}.dl-chat__empty p{max-width:280px;margin:0;color:var(--muted);font-size:13px;line-height:1.5}
1741
+ .dl-chat__thread{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}.dl-chat__thread[hidden],.dl-chat__empty[hidden]{display:none}.dl-chat__thread-header{flex:0 0 auto;height:62px;display:flex;align-items:center;gap:10px;padding:0 16px;border-bottom:1px solid var(--line)}.dl-chat__thread-header .dl-chat__avatar{width:36px;height:36px;flex:0 0 36px}.dl-chat__thread-header>div{min-width:0;display:flex;flex-direction:column}.dl-chat__thread-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px}.dl-chat__thread-status{position:relative;padding-left:11px;color:var(--muted);font-size:11px}.dl-chat__thread-status:before{content:"";position:absolute;left:0;top:50%;width:7px;height:7px;border:1.5px solid #87969d;border-radius:50%;transform:translateY(-50%)}.dl-chat__thread-status.is-online:before{border-color:var(--green);background:var(--green)}.dl-chat__back{display:none;color:var(--teal)}
1742
+ .dl-chat__older{flex:0 0 auto;align-self:center;margin:7px 0 0;padding:4px 8px;border:0;color:var(--teal-deep);background:transparent;font-size:11px;cursor:pointer}.dl-chat__older:hover{text-decoration:underline}.dl-chat__messages{flex:1 1 auto;min-height:0;overflow-y:auto;padding:12px 18px 18px;scroll-behavior:smooth}.dl-chat__date{display:flex;align-items:center;gap:10px;margin:12px 0;color:#71838c;font-size:10px}.dl-chat__date:before,.dl-chat__date:after{content:"";height:1px;flex:1;background:#e3e9ec}.dl-chat__message{display:flex;margin:4px 0}.dl-chat__message.is-mine{justify-content:flex-end}.dl-chat__bubble{max-width:min(76%,430px);padding:8px 10px 6px;color:var(--ink);background:#edf2f4;border-radius:5px 11px 11px 11px;font-size:13px;line-height:1.42;overflow-wrap:anywhere}.dl-chat__message.is-mine .dl-chat__bubble{color:var(--teal-fore);background:var(--teal);border-radius:11px 5px 11px 11px}.dl-chat__bubble>span{white-space:pre-wrap}.dl-chat__bubble time{display:block;margin-top:3px;color:#6c7e87;font-size:9px;text-align:right}.dl-chat__message.is-mine time{color:color-mix(in srgb,var(--teal-fore) 72%,transparent)}.dl-chat__retry{display:block;margin:5px 0 0 auto;padding:2px 7px;border:1px solid rgba(255,255,255,.65);border-radius:4px;color:var(--teal-fore);background:transparent;font-size:10px;cursor:pointer}.dl-chat__retry:hover{background:rgba(255,255,255,.12)}.dl-chat__thread-empty{max-width:300px;margin:auto;padding:24px;color:var(--muted);font-size:12px;line-height:1.5;text-align:center}.dl-chat__typing{flex:0 0 auto;height:20px;padding:0 18px;color:var(--muted);font-size:11px;font-style:italic}
1743
+ .dl-chat__compose{flex:0 0 auto;position:relative;display:grid;grid-template-columns:minmax(0,1fr) 38px;gap:8px;padding:10px 14px 22px;border-top:1px solid var(--line)}.dl-chat__composer{width:100%;min-height:38px;max-height:92px;overflow-y:auto;resize:none;box-sizing:border-box;padding:9px 11px;color:var(--ink);border:1px solid #bdcbd1;border-radius:7px;line-height:1.35;outline:0}.dl-chat__send{display:grid;place-items:center;width:38px;height:38px;padding:0;border:0;border-radius:7px;color:var(--teal-fore);background:var(--teal);cursor:pointer;transition:background-color 120ms ease-out}.dl-chat__send:hover{background:var(--teal-deep)}.dl-chat__send:disabled{cursor:not-allowed;opacity:.45}.dl-chat__send svg{width:19px;height:19px;fill:currentColor}.dl-chat__hint{position:absolute;left:15px;bottom:5px;color:#829098;font-size:9px}
1744
+ .dl-chat__restore{display:none}
1745
+ .dl-chat.is-minimized{left:auto!important;top:auto!important;right:max(22px,env(safe-area-inset-right))!important;bottom:max(22px,env(safe-area-inset-bottom))!important;width:min(310px,calc(100vw - 32px))!important;min-width:0;height:68px!important;min-height:68px;resize:none;border:1px solid var(--teal-deep);border-radius:12px;background:var(--teal);color:var(--teal-fore);box-shadow:0 6px 24px rgba(23,36,43,.24)}
1746
+ .dl-chat.is-minimized .dl-chat__titlebar{position:absolute;inset:0 0 0 auto;height:68px;padding:0 8px;cursor:default;background:transparent;touch-action:auto}
1747
+ .dl-chat.is-minimized .dl-chat__brand{display:contents}
1748
+ .dl-chat.is-minimized .dl-chat__brand>.dl-chat__mark,.dl-chat.is-minimized .dl-chat__brand>strong,.dl-chat.is-minimized .dl-chat__sound,.dl-chat.is-minimized .dl-chat__popout,.dl-chat.is-minimized .dl-chat__minimize{display:none}
1749
+ .dl-chat.is-minimized .dl-chat__title-unread{margin-right:10px}
1750
+ .dl-chat.is-minimized .dl-chat__close{width:44px;height:44px;border:1px solid rgba(255,255,255,.4);border-radius:7px}
1751
+ .dl-chat.is-minimized .dl-chat__restore{display:flex;align-items:center;gap:12px;flex:1;width:100%;padding:10px 110px 10px 16px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;touch-action:manipulation}
1752
+ .dl-chat.is-minimized .dl-chat__restore:hover{background:rgba(255,255,255,.1)}
1753
+ .dl-chat__restore .dl-chat__mark{flex:none;width:32px;height:32px}.dl-chat__restore .dl-chat__mark svg{width:28px;height:28px}
1754
+ .dl-chat__restore strong,.dl-chat__restore small{display:block;white-space:nowrap}.dl-chat__restore strong{font-size:15px}.dl-chat__restore small{margin-top:3px;font-size:11px}
1755
+ .dl-chat.is-minimized .dl-chat__body,.dl-chat.is-minimized .dl-chat__connection,.dl-chat.is-minimized .dl-chat__error{display:none}
1756
+ @container (max-width:520px){.dl-chat__body{grid-template-columns:1fr}.dl-chat__main{display:none}.dl-chat.has-selection .dl-chat__sidebar{display:none}.dl-chat.has-selection .dl-chat__main{display:flex}.dl-chat__back{display:grid}.dl-chat__hint{display:none}.dl-chat__compose{padding-bottom:10px}}
1757
+ @media(max-width:600px){.dl-chat{inset:0;width:100%;height:100dvh;min-width:0;min-height:0;border:0;border-radius:0;resize:none}.dl-chat.is-minimized{right:max(12px,env(safe-area-inset-right))!important;bottom:max(12px,env(safe-area-inset-bottom))!important;width:min(310px,calc(100vw - 24px))!important}.dl-chat__titlebar{cursor:default}.dl-chat__popout{display:none}}
1758
+ @supports not (color:color-mix(in srgb,red,blue)){.dl-chat{--teal-deep:#2f596d;--teal-soft:#e9f1f4;--teal-wash:#f4f7f8;--teal-hover:#e5edef;--teal-selected:#dce8ed}}
1697
1759
  @media(prefers-reduced-motion:reduce){.dl-chat *{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important}}
1698
1760
  `;
1761
+ const SOUND_KEY = "datalynk-chat-sound";
1699
1762
  const windows = /* @__PURE__ */ new WeakMap();
1700
1763
  function openChat(chat, options = {}) {
1701
1764
  const doc = options.document || document;
@@ -1726,6 +1789,8 @@ class ChatWindow {
1726
1789
  __publicField(this, "resizeObserver");
1727
1790
  __publicField(this, "renderedMessageIds", "");
1728
1791
  __publicField(this, "destroyed", false);
1792
+ __publicField(this, "stopReadVisibility");
1793
+ __publicField(this, "onVisibilityChange", () => void this.chat.markLatestRead().catch(() => void 0));
1729
1794
  __publicField(this, "onDestroyed");
1730
1795
  this.chat = chat;
1731
1796
  this.options = options;
@@ -1736,9 +1801,11 @@ class ChatWindow {
1736
1801
  this.root.innerHTML = `<style>${CHAT_CSS}</style>${CHAT_HTML}`;
1737
1802
  this.doc.body.appendChild(this.host);
1738
1803
  this.bind();
1804
+ this.stopReadVisibility = this.chat.setReadVisibility(() => this.canReadSelectedThread());
1739
1805
  this.restoreGeometry();
1740
1806
  this.subscribe();
1741
1807
  this.restoreMinimized();
1808
+ this.renderSound();
1742
1809
  void this.initializeGui();
1743
1810
  this.searchInput.focus();
1744
1811
  }
@@ -1749,20 +1816,24 @@ class ChatWindow {
1749
1816
  this.host.hidden = false;
1750
1817
  this.setMinimized(false);
1751
1818
  this.searchInput.focus();
1819
+ this.onVisibilityChange();
1752
1820
  }
1753
1821
  destroy() {
1754
- var _a, _b, _c, _d, _e, _f;
1822
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1755
1823
  if (this.destroyed) return;
1756
1824
  this.destroyed = true;
1825
+ (_a = this.stopReadVisibility) == null ? void 0 : _a.call(this);
1826
+ this.doc.removeEventListener("visibilitychange", this.onVisibilityChange);
1827
+ (_b = this.doc.defaultView) == null ? void 0 : _b.removeEventListener("focus", this.onVisibilityChange);
1757
1828
  this.subscriptions.forEach((subscription) => subscription.unsubscribe());
1758
- (_a = this.resizeObserver) == null ? void 0 : _a.disconnect();
1829
+ (_c = this.resizeObserver) == null ? void 0 : _c.disconnect();
1759
1830
  clearTimeout(this.searchTimer);
1760
1831
  clearTimeout(this.typingTimer);
1761
1832
  if (this.typingSent && this.selected) void this.chat.setTyping(this.selected.id, false).catch(() => void 0);
1762
1833
  this.host.remove();
1763
- (_c = (_b = this.options).onClose) == null ? void 0 : _c.call(_b);
1764
- (_d = this.onDestroyed) == null ? void 0 : _d.call(this);
1765
- if (this.options.popout && !((_e = this.doc.defaultView) == null ? void 0 : _e.closed)) (_f = this.doc.defaultView) == null ? void 0 : _f.close();
1834
+ (_e = (_d = this.options).onClose) == null ? void 0 : _e.call(_d);
1835
+ (_f = this.onDestroyed) == null ? void 0 : _f.call(this);
1836
+ if (this.options.popout && !((_g = this.doc.defaultView) == null ? void 0 : _g.closed)) (_h = this.doc.defaultView) == null ? void 0 : _h.close();
1766
1837
  }
1767
1838
  get shell() {
1768
1839
  return this.query(".dl-chat");
@@ -1782,23 +1853,49 @@ class ChatWindow {
1782
1853
  get sendButton() {
1783
1854
  return this.query(".dl-chat__send");
1784
1855
  }
1856
+ canReadSelectedThread() {
1857
+ return !this.destroyed && !this.host.hidden && !this.shell.classList.contains("is-minimized") && this.doc.visibilityState === "visible" && Boolean(this.doc.hasFocus()) && this.query(".dl-chat__thread").getClientRects().length > 0;
1858
+ }
1785
1859
  query(selector) {
1786
1860
  const node = this.root.querySelector(selector);
1787
1861
  if (!node) throw new Error(`Missing chat element: ${selector}`);
1788
1862
  return node;
1789
1863
  }
1790
1864
  bind() {
1791
- var _a;
1865
+ var _a, _b;
1792
1866
  this.query(".dl-chat__close").addEventListener("click", () => this.destroy());
1867
+ this.query(".dl-chat__restore").addEventListener("click", () => this.setMinimized(false));
1793
1868
  this.query(".dl-chat__minimize").addEventListener("click", () => this.setMinimized(!this.shell.classList.contains("is-minimized")));
1794
1869
  this.query(".dl-chat__popout").addEventListener("click", () => this.popout());
1870
+ this.query(".dl-chat__sound").addEventListener("click", () => this.setMuted(!this.isMuted()));
1795
1871
  this.query(".dl-chat__back").addEventListener("click", () => {
1796
1872
  this.shell.classList.remove("has-selection");
1797
1873
  this.searchInput.focus();
1798
1874
  });
1799
1875
  this.searchInput.addEventListener("input", () => {
1876
+ this.renderPeople(this.chat.users$.value);
1800
1877
  clearTimeout(this.searchTimer);
1801
- this.searchTimer = setTimeout(() => void this.chat.loadDirectory(this.searchInput.value).catch(() => void 0), 220);
1878
+ this.searchTimer = setTimeout(() => void this.chat.loadDirectory(this.searchInput.value).catch(() => void 0), 150);
1879
+ });
1880
+ this.searchInput.addEventListener("keydown", (event) => {
1881
+ const first = this.query(".dl-chat__people").querySelector(".dl-chat__person");
1882
+ if (event.key === "Enter" && first) {
1883
+ event.preventDefault();
1884
+ first.click();
1885
+ } else if (event.key === "ArrowDown" && first) {
1886
+ event.preventDefault();
1887
+ first.focus();
1888
+ }
1889
+ });
1890
+ this.query(".dl-chat__people").addEventListener("keydown", (event) => {
1891
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1892
+ const buttons = Array.from(this.query(".dl-chat__people").querySelectorAll(".dl-chat__person"));
1893
+ const index = buttons.indexOf(this.doc.activeElement);
1894
+ if (index < 0) return;
1895
+ event.preventDefault();
1896
+ if (event.key === "ArrowDown") buttons[Math.min(index + 1, buttons.length - 1)].focus();
1897
+ else if (index === 0) this.searchInput.focus();
1898
+ else buttons[index - 1].focus();
1802
1899
  });
1803
1900
  this.composer.addEventListener("input", () => this.onComposerInput());
1804
1901
  this.composer.addEventListener("keydown", (event) => {
@@ -1818,6 +1915,8 @@ class ChatWindow {
1818
1915
  this.resizeObserver.observe(this.shell);
1819
1916
  }
1820
1917
  (_a = this.doc.defaultView) == null ? void 0 : _a.addEventListener("beforeunload", () => this.destroy(), { once: true });
1918
+ this.doc.addEventListener("visibilitychange", this.onVisibilityChange);
1919
+ (_b = this.doc.defaultView) == null ? void 0 : _b.addEventListener("focus", this.onVisibilityChange);
1821
1920
  }
1822
1921
  subscribe() {
1823
1922
  this.subscriptions.push(
@@ -1834,10 +1933,18 @@ class ChatWindow {
1834
1933
  this.api.status$.subscribe((status) => this.renderConnection(status)),
1835
1934
  this.chat.unread$.subscribe((count) => {
1836
1935
  var _a, _b;
1837
- return (_b = (_a = this.options).onUnreadChange) == null ? void 0 : _b.call(_a, count);
1936
+ this.renderUnread(count);
1937
+ (_b = (_a = this.options).onUnreadChange) == null ? void 0 : _b.call(_a, count);
1838
1938
  })
1839
1939
  );
1840
1940
  }
1941
+ /** Titlebar count; stays visible while the window is minimized. */
1942
+ renderUnread(count) {
1943
+ const badge = this.query(".dl-chat__title-unread");
1944
+ badge.toggleAttribute("hidden", count <= 0);
1945
+ badge.textContent = count > 99 ? "99+" : String(count);
1946
+ badge.setAttribute("aria-label", `${count} unread message${count === 1 ? "" : "s"}`);
1947
+ }
1841
1948
  renderPeople(users) {
1842
1949
  const query = this.searchInput.value.trim().toLowerCase();
1843
1950
  const conversationIds = new Set(this.chat.conversations$.value.map((row) => row.peer.id));
@@ -1847,11 +1954,11 @@ class ChatWindow {
1847
1954
  if (!filtered.length) {
1848
1955
  const empty = this.doc.createElement("p");
1849
1956
  empty.className = "dl-chat__list-empty";
1850
- empty.textContent = query ? "No matching people" : "Search for someone to start a conversation.";
1957
+ empty.textContent = query ? "No matching people" : "No other people to start a conversation with.";
1851
1958
  target.appendChild(empty);
1852
1959
  return;
1853
1960
  }
1854
- filtered.slice(0, query ? 30 : 8).forEach((user) => target.appendChild(this.personButton(user)));
1961
+ filtered.slice(0, 10).forEach((user) => target.appendChild(this.personButton(user)));
1855
1962
  }
1856
1963
  renderConversations(rows) {
1857
1964
  this.conversationList.replaceChildren();
@@ -1993,7 +2100,13 @@ class ChatWindow {
1993
2100
  node.textContent = status === "unauthorized" ? "Your session expired. Sign in again to continue chatting." : "Chat is reconnecting. Messages cannot be sent yet.";
1994
2101
  this.sendButton.disabled = status !== "online" || !this.composer.value.trim();
1995
2102
  }
2103
+ growComposer() {
2104
+ const box = this.composer;
2105
+ box.style.height = "auto";
2106
+ box.style.height = `${box.scrollHeight + box.offsetHeight - box.clientHeight}px`;
2107
+ }
1996
2108
  onComposerInput() {
2109
+ this.growComposer();
1997
2110
  if (!this.selected) return;
1998
2111
  this.writeDraft(this.selected.id, this.composer.value);
1999
2112
  this.sendButton.disabled = !this.composer.value.trim() || this.api.status !== "online";
@@ -2017,6 +2130,7 @@ class ChatWindow {
2017
2130
  try {
2018
2131
  await this.chat.send(peer.id, body);
2019
2132
  this.composer.value = "";
2133
+ this.growComposer();
2020
2134
  this.writeDraft(peer.id, "");
2021
2135
  if (this.typingSent) await this.chat.setTyping(peer.id, false).catch(() => void 0);
2022
2136
  this.typingSent = false;
@@ -2027,7 +2141,7 @@ class ChatWindow {
2027
2141
  }
2028
2142
  }
2029
2143
  popout() {
2030
- var _a;
2144
+ var _a, _b;
2031
2145
  const popup = (_a = this.doc.defaultView) == null ? void 0 : _a.open("", "datalynk-chat", "popup,width=760,height=720,resizable=yes");
2032
2146
  if (!popup) {
2033
2147
  this.chat.error$.next("Allow pop-ups to open chat in a separate window.");
@@ -2035,6 +2149,11 @@ class ChatWindow {
2035
2149
  }
2036
2150
  popup.document.title = this.options.title || "Datalynk Chat";
2037
2151
  popup.document.body.style.margin = "0";
2152
+ const theme = (_b = this.doc.defaultView) == null ? void 0 : _b.getComputedStyle(this.doc.documentElement);
2153
+ ["--primary", "--primary-fore", "--accent", "--accentText"].forEach((name) => {
2154
+ const value = theme == null ? void 0 : theme.getPropertyValue(name).trim();
2155
+ if (value) popup.document.documentElement.style.setProperty(name, value);
2156
+ });
2038
2157
  openChat(this.chat, { ...this.options, document: popup.document, popout: true });
2039
2158
  this.destroy();
2040
2159
  }
@@ -2080,6 +2199,7 @@ class ChatWindow {
2080
2199
  async initializeGui() {
2081
2200
  try {
2082
2201
  await this.chat.initialize();
2202
+ void this.chat.loadDirectory(this.searchInput.value).catch(() => void 0);
2083
2203
  const selectedId = this.readSelectedUser();
2084
2204
  if (!selectedId || this.chat.selectedUser$.value) return;
2085
2205
  const conversation = this.chat.conversations$.value.find((row) => row.peer.id === selectedId);
@@ -2090,6 +2210,30 @@ class ChatWindow {
2090
2210
  } catch {
2091
2211
  }
2092
2212
  }
2213
+ isMuted() {
2214
+ try {
2215
+ return localStorage.getItem(SOUND_KEY) === "off";
2216
+ } catch {
2217
+ return false;
2218
+ }
2219
+ }
2220
+ setMuted(muted) {
2221
+ try {
2222
+ if (muted) localStorage.setItem(SOUND_KEY, "off");
2223
+ else localStorage.removeItem(SOUND_KEY);
2224
+ } catch {
2225
+ }
2226
+ this.renderSound();
2227
+ }
2228
+ renderSound() {
2229
+ var _a;
2230
+ const muted = this.isMuted();
2231
+ const button = this.query(".dl-chat__sound");
2232
+ button.setAttribute("aria-pressed", String(muted));
2233
+ button.setAttribute("aria-label", muted ? "Unmute message sound" : "Mute message sound");
2234
+ button.title = muted ? "Unmute message sound" : "Mute message sound";
2235
+ (_a = button.querySelector("path")) == null ? void 0 : _a.setAttribute("d", muted ? SOUND_OFF_ICON : SOUND_ON_ICON);
2236
+ }
2093
2237
  restoreMinimized() {
2094
2238
  try {
2095
2239
  if (localStorage.getItem(this.storageKey("minimized")) === "1") this.setMinimized(true, false);
@@ -2101,11 +2245,17 @@ class ChatWindow {
2101
2245
  if (this.options.popout) return;
2102
2246
  if (minimized && !this.shell.classList.contains("is-minimized")) this.saveGeometry();
2103
2247
  this.shell.classList.toggle("is-minimized", minimized);
2248
+ this.query(".dl-chat__restore").hidden = !minimized;
2104
2249
  const button = this.query(".dl-chat__minimize");
2105
2250
  button.setAttribute("aria-label", minimized ? "Restore chat" : "Minimize chat");
2106
2251
  button.title = minimized ? "Restore" : "Minimize";
2107
2252
  (_a = button.querySelector("path")) == null ? void 0 : _a.setAttribute("d", minimized ? "M7.4 15.4 12 10.8l4.6 4.6L18 14l-6-6-6 6 1.4 1.4z" : "M5 11h14v2H5z");
2108
2253
  if (!minimized) this.restoreGeometry();
2254
+ if (!minimized) this.onVisibilityChange();
2255
+ if (persist) {
2256
+ if (minimized) this.query(".dl-chat__restore").focus();
2257
+ else this.searchInput.focus();
2258
+ }
2109
2259
  if (persist) this.saveMinimized(minimized);
2110
2260
  }
2111
2261
  saveMinimized(value) {
@@ -2134,22 +2284,41 @@ class ChatWindow {
2134
2284
  var _a, _b;
2135
2285
  if (this.options.popout) return;
2136
2286
  try {
2287
+ const viewWidth = ((_a = this.doc.defaultView) == null ? void 0 : _a.innerWidth) || 760;
2288
+ const viewHeight = ((_b = this.doc.defaultView) == null ? void 0 : _b.innerHeight) || 720;
2289
+ if (viewWidth <= 600) {
2290
+ ["width", "height", "left", "top", "right", "bottom"].forEach((key) => this.shell.style.removeProperty(key));
2291
+ return;
2292
+ }
2137
2293
  const stored = JSON.parse(localStorage.getItem(this.storageKey("geometry")) || "null");
2138
- if (!stored) return;
2139
- const maxWidth = Math.max(360, (((_a = this.doc.defaultView) == null ? void 0 : _a.innerWidth) || 760) - 16);
2140
- const maxHeight = Math.max(460, (((_b = this.doc.defaultView) == null ? void 0 : _b.innerHeight) || 720) - 16);
2141
- this.shell.style.width = Math.min(maxWidth, Math.max(360, stored.width || 720)) + "px";
2142
- this.shell.style.height = Math.min(maxHeight, Math.max(460, stored.height || 640)) + "px";
2143
- this.shell.style.left = Math.max(0, Math.min(stored.left || 8, maxWidth - 120)) + "px";
2144
- this.shell.style.top = Math.max(0, Math.min(stored.top || 8, maxHeight - 60)) + "px";
2145
- this.shell.style.right = "auto";
2146
- this.shell.style.bottom = "auto";
2294
+ const valid = stored && [stored.width, stored.height, stored.left, stored.top].every((value) => typeof value === "number" && Number.isFinite(value)) && stored.width > 0 && stored.height > 0;
2295
+ const margin = this.options.placement === "bottom-right" ? 22 : 8;
2296
+ if (valid) {
2297
+ const width = Math.min(viewWidth - margin * 2, Math.max(560, stored.width));
2298
+ const height = Math.min(viewHeight - margin * 2, Math.max(460, stored.height));
2299
+ this.shell.style.width = width + "px";
2300
+ this.shell.style.height = height + "px";
2301
+ if (this.options.placement !== "bottom-right") {
2302
+ const clamp = (value, max) => Math.max(margin, Math.min(value, max));
2303
+ this.shell.style.left = clamp(stored.left, viewWidth - width - margin) + "px";
2304
+ this.shell.style.top = clamp(stored.top, viewHeight - height - margin) + "px";
2305
+ this.shell.style.right = "auto";
2306
+ this.shell.style.bottom = "auto";
2307
+ }
2308
+ }
2309
+ if (this.options.placement === "bottom-right") {
2310
+ this.shell.style.left = "auto";
2311
+ this.shell.style.top = "auto";
2312
+ this.shell.style.right = margin + "px";
2313
+ this.shell.style.bottom = margin + "px";
2314
+ }
2147
2315
  } catch {
2148
2316
  }
2149
2317
  }
2150
2318
  saveGeometry() {
2151
2319
  if (this.options.popout || this.destroyed || this.shell.classList.contains("is-minimized")) return;
2152
2320
  const rect = this.shell.getBoundingClientRect();
2321
+ if (!this.host.isConnected || this.host.hidden || rect.width <= 0 || rect.height <= 0) return;
2153
2322
  try {
2154
2323
  localStorage.setItem(this.storageKey("geometry"), JSON.stringify({
2155
2324
  left: rect.left,