@unhingged/vizu-core 0.1.13 → 0.1.15

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/auto.js CHANGED
@@ -1239,8 +1239,50 @@ var Popover = class {
1239
1239
  this.mentionPicker = null;
1240
1240
  /** Dismiss handlers wired to document for the open picker; cleared in closeMentionPicker. */
1241
1241
  this.mentionPickerDismiss = null;
1242
+ /** All mentionable users fetched for the open picker — pre-filter list. */
1243
+ this.mentionPickerUsers = [];
1244
+ /** Currently visible subset of `mentionPickerUsers` after the user's filter. */
1245
+ this.mentionPickerFiltered = [];
1246
+ /** Highlighted index in `mentionPickerFiltered`. Up/Down arrows mutate this. */
1247
+ this.mentionPickerSelected = 0;
1248
+ /**
1249
+ * 'manual' = opened from the @ mention button (separate search input).
1250
+ * 'editor' = opened by typing `@` in the editor; filter is derived
1251
+ * from the text after `@` in the editor itself, no search input.
1252
+ */
1253
+ this.mentionPickerMode = "manual";
1254
+ /**
1255
+ * In editor mode: a Range covering `@<query>` in the editor's text.
1256
+ * Used to replace that span with the mention chip on commit, and to
1257
+ * recompute the bounding rect when re-positioning after typing.
1258
+ */
1259
+ this.mentionPickerEditorRange = null;
1242
1260
  this.handleEditorKey = (e) => {
1243
1261
  const editor = e.currentTarget;
1262
+ if (this.mentionPicker && this.mentionPickerMode === "editor") {
1263
+ if (e.key === "ArrowDown") {
1264
+ e.preventDefault();
1265
+ this.moveMentionPickerSelection(1);
1266
+ return;
1267
+ }
1268
+ if (e.key === "ArrowUp") {
1269
+ e.preventDefault();
1270
+ this.moveMentionPickerSelection(-1);
1271
+ return;
1272
+ }
1273
+ if (e.key === "Enter" || e.key === "Tab") {
1274
+ if (this.mentionPickerFiltered.length > 0) {
1275
+ e.preventDefault();
1276
+ this.commitMentionPickerSelection();
1277
+ return;
1278
+ }
1279
+ }
1280
+ if (e.key === "Escape") {
1281
+ e.preventDefault();
1282
+ this.closeMentionPicker();
1283
+ return;
1284
+ }
1285
+ }
1244
1286
  if (e.key === "#") {
1245
1287
  e.preventDefault();
1246
1288
  const savedRange = this.saveRange();
@@ -1263,6 +1305,21 @@ var Popover = class {
1263
1305
  this.callbacks.onClose();
1264
1306
  }
1265
1307
  };
1308
+ /**
1309
+ * Fires after every keystroke in the editor (post-insertion). Looks
1310
+ * for an `@<query>` pattern adjacent to the caret. If found, opens
1311
+ * (or refreshes) the mention picker in editor mode. If the trigger
1312
+ * disappears (user typed a space, backspaced over @, etc.), closes
1313
+ * the picker.
1314
+ */
1315
+ this.handleEditorInput = () => {
1316
+ const trigger = this.detectMentionTrigger();
1317
+ if (trigger) {
1318
+ void this.openOrUpdateEditorMentionPicker(trigger.range, trigger.query);
1319
+ } else if (this.mentionPicker && this.mentionPickerMode === "editor") {
1320
+ this.closeMentionPicker();
1321
+ }
1322
+ };
1266
1323
  this.handleEditorPaste = (e) => {
1267
1324
  if (this.callbacks.canUploadAttachments()) {
1268
1325
  const items = e.clipboardData?.items ?? null;
@@ -1523,6 +1580,7 @@ var Popover = class {
1523
1580
  const editor = this.el.querySelector(".vz-textarea");
1524
1581
  editor.focus();
1525
1582
  editor.addEventListener("keydown", this.handleEditorKey);
1583
+ editor.addEventListener("input", this.handleEditorInput);
1526
1584
  editor.addEventListener("paste", this.handleEditorPaste);
1527
1585
  const wrap = this.el.querySelector(".vz-editor-wrap");
1528
1586
  if (wrap && this.callbacks.canUploadAttachments()) {
@@ -1552,6 +1610,37 @@ var Popover = class {
1552
1610
  <div class="vz-anchor-list">${chips}</div>
1553
1611
  `;
1554
1612
  }
1613
+ /**
1614
+ * Walk back from the caret looking for the most recent `@` that
1615
+ * starts a mention trigger — must be at start-of-text-node or
1616
+ * preceded by whitespace, and there must be no whitespace between
1617
+ * `@` and the caret. Returns the Range covering `@<query>` plus
1618
+ * the bare query string.
1619
+ */
1620
+ detectMentionTrigger() {
1621
+ const sel = window.getSelection();
1622
+ if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null;
1623
+ const caret = sel.getRangeAt(0);
1624
+ const node = caret.startContainer;
1625
+ if (node.nodeType !== Node.TEXT_NODE) return null;
1626
+ const text = node.textContent ?? "";
1627
+ const offset = caret.startOffset;
1628
+ for (let i = offset - 1; i >= 0; i--) {
1629
+ const ch = text[i];
1630
+ if (ch === "@") {
1631
+ const prev = i > 0 ? text[i - 1] : "";
1632
+ if (prev !== "" && !/\s/.test(prev)) return null;
1633
+ const query = text.slice(i + 1, offset);
1634
+ if (/\s/.test(query)) return null;
1635
+ const range = document.createRange();
1636
+ range.setStart(node, i);
1637
+ range.setEnd(node, offset);
1638
+ return { range, query };
1639
+ }
1640
+ if (/\s/.test(ch)) return null;
1641
+ }
1642
+ return null;
1643
+ }
1555
1644
  async uploadAndAppend(file) {
1556
1645
  if (!this.callbacks.canUploadAttachments()) return;
1557
1646
  const placeholderId = "upload-" + Math.random().toString(36).slice(2, 9);
@@ -1682,54 +1771,195 @@ var Popover = class {
1682
1771
  return parts.join("").trim();
1683
1772
  }
1684
1773
  /**
1685
- * Open a dropdown listing the workspace's mentionable users. The user
1686
- * clicks one to insert a mention chip for them; Esc or an outside
1687
- * click closes the dropdown.
1688
- *
1689
- * The dropdown is fetched fresh on every open so member adds/removes
1690
- * in another tab are picked up without a popover reopen. Falls back
1691
- * to silent no-op if `onMentionSearch` isn't wired (non-cloud
1692
- * adapters) or returns an empty list.
1774
+ * Manual-mode picker: opens from the @ mention button. Renders a
1775
+ * search input above the list; the user types into the input to
1776
+ * filter, arrows + Enter to pick. The selection inserts a mention
1777
+ * chip at the editor's current caret (no `@<query>` replacement —
1778
+ * the user didn't type one).
1693
1779
  */
1694
1780
  async openMentionPicker(anchor) {
1695
1781
  this.closeMentionPicker();
1696
1782
  if (!this.callbacks.onMentionSearch) return;
1783
+ this.mentionPickerMode = "manual";
1784
+ this.mentionPickerEditorRange = null;
1785
+ const picker = this.createMentionPickerShell();
1786
+ this.positionMentionPickerByElement(picker, anchor);
1787
+ const users = await this.fetchMentionables();
1788
+ if (this.mentionPicker !== picker) return;
1789
+ this.mentionPickerUsers = users;
1790
+ this.renderMentionPickerBody("");
1791
+ this.positionMentionPickerByElement(picker, anchor);
1792
+ const input = picker.querySelector(".vz-mention-search");
1793
+ input?.focus();
1794
+ this.wireMentionPickerDismiss();
1795
+ }
1796
+ /**
1797
+ * Editor-mode picker: opens because the user typed `@` in the
1798
+ * editor. No search input — the editor IS the search; the query is
1799
+ * the text after `@`. On commit, replaces the `@<query>` range with
1800
+ * the chip.
1801
+ */
1802
+ async openOrUpdateEditorMentionPicker(range, query) {
1803
+ this.mentionPickerEditorRange = range;
1804
+ if (!this.mentionPicker || this.mentionPickerMode !== "editor") {
1805
+ this.closeMentionPicker();
1806
+ this.mentionPickerMode = "editor";
1807
+ const picker = this.createMentionPickerShell({ showSearch: false });
1808
+ this.positionMentionPickerByRange(picker, range);
1809
+ const users = await this.fetchMentionables();
1810
+ if (this.mentionPicker !== picker) return;
1811
+ this.mentionPickerUsers = users;
1812
+ this.renderMentionPickerBody(query);
1813
+ this.positionMentionPickerByRange(picker, range);
1814
+ this.wireMentionPickerDismiss();
1815
+ return;
1816
+ }
1817
+ this.renderMentionPickerBody(query);
1818
+ this.positionMentionPickerByRange(this.mentionPicker, range);
1819
+ }
1820
+ /**
1821
+ * Build the picker DOM scaffold and attach it to the popover. Does
1822
+ * NOT fetch users or render the body — caller's responsibility.
1823
+ * Search input is hidden in editor mode where the user already has
1824
+ * a perfectly good text-entry surface (the editor itself).
1825
+ */
1826
+ createMentionPickerShell({ showSearch = true } = {}) {
1697
1827
  const picker = document.createElement("div");
1698
1828
  picker.className = "vz-mention-picker";
1699
1829
  picker.setAttribute("data-vz-ignore", "");
1700
- picker.innerHTML = `<div class="vz-mention-loading">Loading\u2026</div>`;
1830
+ picker.innerHTML = `
1831
+ ${showSearch ? `<input class="vz-mention-search" type="text" placeholder="Search teammates\u2026" autocomplete="off" spellcheck="false" />` : ""}
1832
+ <div class="vz-mention-list" data-vz="mention-list">
1833
+ <div class="vz-mention-loading">Loading\u2026</div>
1834
+ </div>
1835
+ `;
1701
1836
  this.el.appendChild(picker);
1702
1837
  this.mentionPicker = picker;
1703
- this.positionMentionPicker(picker, anchor);
1704
- let users = [];
1838
+ this.mentionPickerFiltered = [];
1839
+ this.mentionPickerSelected = 0;
1840
+ picker.addEventListener("mousemove", (e) => {
1841
+ const btn = e.target?.closest(".vz-mention-item");
1842
+ if (!btn) return;
1843
+ const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
1844
+ if (idx >= 0 && idx !== this.mentionPickerSelected) {
1845
+ this.mentionPickerSelected = idx;
1846
+ this.refreshMentionPickerHighlight();
1847
+ }
1848
+ });
1849
+ picker.addEventListener("click", (e) => {
1850
+ const btn = e.target?.closest(".vz-mention-item");
1851
+ if (!btn) return;
1852
+ e.preventDefault();
1853
+ e.stopPropagation();
1854
+ const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
1855
+ if (idx >= 0) {
1856
+ this.mentionPickerSelected = idx;
1857
+ this.commitMentionPickerSelection();
1858
+ }
1859
+ });
1860
+ if (showSearch) {
1861
+ const input = picker.querySelector(".vz-mention-search");
1862
+ input?.addEventListener("input", () => this.renderMentionPickerBody(input.value));
1863
+ input?.addEventListener("keydown", (e) => {
1864
+ if (e.key === "ArrowDown") {
1865
+ e.preventDefault();
1866
+ this.moveMentionPickerSelection(1);
1867
+ } else if (e.key === "ArrowUp") {
1868
+ e.preventDefault();
1869
+ this.moveMentionPickerSelection(-1);
1870
+ } else if (e.key === "Enter" || e.key === "Tab") {
1871
+ if (this.mentionPickerFiltered.length > 0) {
1872
+ e.preventDefault();
1873
+ this.commitMentionPickerSelection();
1874
+ }
1875
+ } else if (e.key === "Escape") {
1876
+ e.preventDefault();
1877
+ this.closeMentionPicker();
1878
+ }
1879
+ });
1880
+ }
1881
+ return picker;
1882
+ }
1883
+ /** Fetch the mentionable list. Swallows errors → []. */
1884
+ async fetchMentionables() {
1885
+ if (!this.callbacks.onMentionSearch) return [];
1705
1886
  try {
1706
- users = await this.callbacks.onMentionSearch();
1887
+ return await this.callbacks.onMentionSearch();
1707
1888
  } catch {
1889
+ return [];
1708
1890
  }
1709
- if (this.mentionPicker !== picker) return;
1710
- if (users.length === 0) {
1711
- picker.innerHTML = `<div class="vz-mention-empty">No teammates to mention yet.</div>`;
1712
- this.positionMentionPicker(picker, anchor);
1713
- this.wireMentionPickerDismiss();
1891
+ }
1892
+ /**
1893
+ * Re-filter the cached users by `query`, render the list, reset
1894
+ * selected to 0. Called on every search input change and on every
1895
+ * editor input event while in editor mode.
1896
+ */
1897
+ renderMentionPickerBody(query) {
1898
+ const picker = this.mentionPicker;
1899
+ if (!picker) return;
1900
+ const list = picker.querySelector('[data-vz="mention-list"]');
1901
+ if (!list) return;
1902
+ const q = query.toLowerCase().trim();
1903
+ this.mentionPickerFiltered = q ? this.mentionPickerUsers.filter((u) => u.name.toLowerCase().includes(q)) : this.mentionPickerUsers.slice();
1904
+ this.mentionPickerSelected = 0;
1905
+ if (this.mentionPickerFiltered.length === 0) {
1906
+ list.innerHTML = `<div class="vz-mention-empty">${this.mentionPickerUsers.length === 0 ? "No teammates to mention yet." : "No teammates match."}</div>`;
1714
1907
  return;
1715
1908
  }
1716
- const itemsHtml = users.map((u) => {
1909
+ list.innerHTML = this.mentionPickerFiltered.map((u, i) => {
1717
1910
  const avatar = u.avatarUrl ? `<img class="vz-mention-avatar" src="${escapeHtml(u.avatarUrl)}" alt="" />` : `<span class="vz-mention-avatar vz-mention-avatar-initials">${escapeHtml(initials(u.name) || "?")}</span>`;
1718
- return `<button class="vz-mention-item" type="button" data-mention-id="${escapeHtml(u.id)}" data-mention-name="${escapeHtml(u.name)}">${avatar}<span class="vz-mention-item-name">${escapeHtml(u.name)}</span></button>`;
1911
+ return `<button class="vz-mention-item${i === 0 ? " is-selected" : ""}" type="button" data-mention-index="${i}">${avatar}<span class="vz-mention-item-name">${escapeHtml(u.name)}</span></button>`;
1719
1912
  }).join("");
1720
- picker.innerHTML = itemsHtml;
1721
- this.positionMentionPicker(picker, anchor);
1722
- picker.addEventListener("click", (e) => {
1723
- const btn = e.target?.closest(".vz-mention-item");
1724
- if (!btn) return;
1725
- e.preventDefault();
1726
- e.stopPropagation();
1727
- const id = btn.getAttribute("data-mention-id") || "";
1728
- const name = btn.getAttribute("data-mention-name") || "";
1729
- if (id && name) this.insertMentionChip({ id, name });
1730
- this.closeMentionPicker();
1731
- });
1732
- this.wireMentionPickerDismiss();
1913
+ }
1914
+ moveMentionPickerSelection(delta) {
1915
+ if (this.mentionPickerFiltered.length === 0) return;
1916
+ const n = this.mentionPickerFiltered.length;
1917
+ this.mentionPickerSelected = (this.mentionPickerSelected + delta + n) % n;
1918
+ this.refreshMentionPickerHighlight();
1919
+ }
1920
+ refreshMentionPickerHighlight() {
1921
+ const picker = this.mentionPicker;
1922
+ if (!picker) return;
1923
+ const items = picker.querySelectorAll(".vz-mention-item");
1924
+ items.forEach((el, i) => el.classList.toggle("is-selected", i === this.mentionPickerSelected));
1925
+ items[this.mentionPickerSelected]?.scrollIntoView({ block: "nearest" });
1926
+ }
1927
+ commitMentionPickerSelection() {
1928
+ const user = this.mentionPickerFiltered[this.mentionPickerSelected];
1929
+ if (!user) return;
1930
+ if (this.mentionPickerMode === "editor" && this.mentionPickerEditorRange) {
1931
+ this.replaceRangeWithMentionChip(this.mentionPickerEditorRange, user);
1932
+ } else {
1933
+ this.insertMentionChip(user);
1934
+ }
1935
+ this.closeMentionPicker();
1936
+ }
1937
+ /**
1938
+ * Swap a Range covering `@<query>` for a mention chip + trailing
1939
+ * space, then place the caret after the space so the user can keep
1940
+ * typing. Used only in editor-trigger mode.
1941
+ */
1942
+ replaceRangeWithMentionChip(range, user) {
1943
+ const editor = this.el.querySelector(".vz-textarea");
1944
+ if (!editor) return;
1945
+ const chip = document.createElement("span");
1946
+ chip.className = "vz-chip vz-chip-mention";
1947
+ chip.contentEditable = "false";
1948
+ chip.setAttribute("data-mention-id", user.id);
1949
+ chip.textContent = "@" + user.name;
1950
+ range.deleteContents();
1951
+ range.insertNode(chip);
1952
+ const space = document.createTextNode(" ");
1953
+ chip.after(space);
1954
+ const sel = window.getSelection();
1955
+ if (sel) {
1956
+ const after = document.createRange();
1957
+ after.setStartAfter(space);
1958
+ after.collapse(true);
1959
+ sel.removeAllRanges();
1960
+ sel.addRange(after);
1961
+ }
1962
+ editor.focus();
1733
1963
  }
1734
1964
  /** Wires Esc + outside-click handlers to close the open picker. */
1735
1965
  wireMentionPickerDismiss() {
@@ -1743,6 +1973,10 @@ var Popover = class {
1743
1973
  const onOutside = (e) => {
1744
1974
  if (!this.mentionPicker) return;
1745
1975
  if (this.mentionPicker.contains(e.target)) return;
1976
+ if (this.mentionPickerMode === "editor") {
1977
+ const editor = this.el.querySelector(".vz-textarea");
1978
+ if (editor && editor.contains(e.target)) return;
1979
+ }
1746
1980
  this.closeMentionPicker();
1747
1981
  };
1748
1982
  document.addEventListener("keydown", onKey, true);
@@ -1761,18 +1995,23 @@ var Popover = class {
1761
1995
  this.mentionPicker.remove();
1762
1996
  this.mentionPicker = null;
1763
1997
  }
1998
+ this.mentionPickerFiltered = [];
1999
+ this.mentionPickerSelected = 0;
2000
+ this.mentionPickerEditorRange = null;
1764
2001
  }
1765
- /**
1766
- * Place the picker below the anchor button. Stays within the
1767
- * popover's right edge so we don't overflow the popover frame.
1768
- */
1769
- positionMentionPicker(picker, anchor) {
2002
+ /** Position the picker below the given anchor (the @ mention button). */
2003
+ positionMentionPickerByElement(picker, anchor) {
1770
2004
  const popRect = this.el.getBoundingClientRect();
1771
2005
  const btnRect = anchor.getBoundingClientRect();
1772
- const left = btnRect.left - popRect.left;
1773
- const top = btnRect.bottom - popRect.top + 4;
1774
- picker.style.left = `${left}px`;
1775
- picker.style.top = `${top}px`;
2006
+ picker.style.left = `${btnRect.left - popRect.left}px`;
2007
+ picker.style.top = `${btnRect.bottom - popRect.top + 4}px`;
2008
+ }
2009
+ /** Position the picker below the `@<query>` text range in the editor. */
2010
+ positionMentionPickerByRange(picker, range) {
2011
+ const popRect = this.el.getBoundingClientRect();
2012
+ const rect = range.getBoundingClientRect();
2013
+ picker.style.left = `${rect.left - popRect.left}px`;
2014
+ picker.style.top = `${rect.bottom - popRect.top + 4}px`;
1776
2015
  }
1777
2016
  /**
1778
2017
  * Insert a mention chip for the given user into the editor at the
@@ -2651,6 +2890,26 @@ var STYLES = `
2651
2890
  flex-direction: column;
2652
2891
  gap: 1px;
2653
2892
  }
2893
+ .vz-mention-search {
2894
+ width: 100%;
2895
+ padding: 7px 9px;
2896
+ margin-bottom: 4px;
2897
+ background: rgba(255,255,255,0.04);
2898
+ border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
2899
+ border-radius: 6px;
2900
+ color: #fafafa;
2901
+ font-size: 13px;
2902
+ outline: none;
2903
+ box-sizing: border-box;
2904
+ }
2905
+ .vz-mention-search:focus { border-color: var(--vz-accent, #ff8b6e); }
2906
+ .vz-mention-search::placeholder { color: var(--vz-text-muted); }
2907
+ .vz-mention-list {
2908
+ display: flex;
2909
+ flex-direction: column;
2910
+ gap: 1px;
2911
+ min-height: 0;
2912
+ }
2654
2913
  .vz-mention-loading,
2655
2914
  .vz-mention-empty {
2656
2915
  padding: 10px 12px;
@@ -2672,9 +2931,9 @@ var STYLES = `
2672
2931
  cursor: pointer;
2673
2932
  transition: background 80ms;
2674
2933
  }
2675
- .vz-mention-item:hover,
2934
+ .vz-mention-item.is-selected,
2676
2935
  .vz-mention-item:focus-visible {
2677
- background: var(--vz-bg-hover, rgba(255,255,255,0.06));
2936
+ background: var(--vz-bg-hover, rgba(255,255,255,0.08));
2678
2937
  outline: none;
2679
2938
  }
2680
2939
  .vz-mention-avatar {
@@ -2977,7 +3236,9 @@ var STYLES = `
2977
3236
  .vz-ref:hover { background: color-mix(in srgb, var(--vz-accent) 30%, var(--vz-bg-hover)); transform: translateY(-1px); }
2978
3237
  .vz-ref::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--vz-accent); flex-shrink: 0; }
2979
3238
 
2980
- /* Inline @mention chip (rendered inside displayed comments) */
3239
+ /* Inline @mention chip (rendered inside displayed comments). user-select
3240
+ * prevents the user from selecting the chip's text and editing it; the
3241
+ * displayed comment is intentionally read-only after save. */
2981
3242
  .vz-mention {
2982
3243
  display: inline-flex; align-items: center;
2983
3244
  padding: 1px 8px 1px 8px;
@@ -2987,13 +3248,21 @@ var STYLES = `
2987
3248
  font-size: 12px; font-weight: 600;
2988
3249
  vertical-align: baseline;
2989
3250
  margin: 0 1px;
3251
+ user-select: none;
3252
+ -webkit-user-select: none;
3253
+ cursor: default;
2990
3254
  }
2991
3255
 
2992
- /* Mention chip inside the editor (contenteditable=false token) */
3256
+ /* Mention chip inside the editor (contenteditable=false token). Same
3257
+ * non-selection rules as .vz-mention so the chip stays atomic even
3258
+ * inside the editable surface \u2014 caret can land before/after but not
3259
+ * inside, no partial-text edit. */
2993
3260
  .vz-chip-mention {
2994
3261
  background: color-mix(in srgb, var(--vz-accent) 16%, var(--vz-bg-hover));
2995
3262
  color: var(--vz-accent);
2996
3263
  font-weight: 600;
3264
+ user-select: none;
3265
+ -webkit-user-select: none;
2997
3266
  }
2998
3267
  .vz-chip-mention::before { display: none; }
2999
3268
 
@@ -3687,10 +3956,29 @@ var Vizu = class {
3687
3956
  replies: [...this.comments[idx].replies ?? [], reply]
3688
3957
  };
3689
3958
  this.comments[idx] = updated;
3690
- if (this.storage.addReply) {
3691
- await this.storage.addReply(this.opts.namespace, commentId, reply);
3692
- } else {
3693
- await this.storage.updateComment(this.opts.namespace, commentId, { replies: updated.replies });
3959
+ try {
3960
+ if (this.storage.addReply) {
3961
+ await this.storage.addReply(this.opts.namespace, commentId, reply);
3962
+ } else {
3963
+ await this.storage.updateComment(this.opts.namespace, commentId, { replies: updated.replies });
3964
+ }
3965
+ } catch (err) {
3966
+ const cur = this.comments[idx];
3967
+ this.comments[idx] = {
3968
+ ...cur,
3969
+ replies: (cur.replies ?? []).filter((r) => r.id !== reply.id)
3970
+ };
3971
+ const code = err?.code ?? "";
3972
+ const status = err?.status;
3973
+ const msg = status === 403 || code === "forbidden" ? "Reply failed \u2014 you don't have permission." : status === 401 || code === "auth_required" ? "Reply failed \u2014 please sign in again." : "Reply failed. Please try again.";
3974
+ this.pill?.toast(msg);
3975
+ this.refreshUi();
3976
+ if (this.popover?.isOpen()) {
3977
+ const anchors = this.popover.getAnchors();
3978
+ if (anchors[0]) this.popover.update(this.commentsForElement(anchors[0]));
3979
+ }
3980
+ if (typeof console !== "undefined") console.warn("[vizu] addReply failed", err);
3981
+ return null;
3694
3982
  }
3695
3983
  this.bus.emit("comment:updated", { comment: updated });
3696
3984
  this.refreshUi();