@unhingged/vizu-core 0.1.12 → 0.1.14

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
@@ -1235,8 +1235,54 @@ var Popover = class {
1235
1235
  this.pendingRefs = {};
1236
1236
  /** Attachments uploaded during this session, persisted into `comment.attachments` on save. */
1237
1237
  this.pendingAttachments = [];
1238
+ /** Open mention picker element, if any. Held so the next openMentionPicker / close calls can tear it down. */
1239
+ this.mentionPicker = null;
1240
+ /** Dismiss handlers wired to document for the open picker; cleared in closeMentionPicker. */
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;
1238
1260
  this.handleEditorKey = (e) => {
1239
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
+ }
1240
1286
  if (e.key === "#") {
1241
1287
  e.preventDefault();
1242
1288
  const savedRange = this.saveRange();
@@ -1259,6 +1305,21 @@ var Popover = class {
1259
1305
  this.callbacks.onClose();
1260
1306
  }
1261
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
+ };
1262
1323
  this.handleEditorPaste = (e) => {
1263
1324
  if (this.callbacks.canUploadAttachments()) {
1264
1325
  const items = e.clipboardData?.items ?? null;
@@ -1341,7 +1402,7 @@ var Popover = class {
1341
1402
  else if (action === "save") this.save();
1342
1403
  else if (action === "mention") {
1343
1404
  e.preventDefault();
1344
- this.insertMentionChip(this.currentUser);
1405
+ this.openMentionPicker(target);
1345
1406
  } else if (action === "upload") {
1346
1407
  e.preventDefault();
1347
1408
  const input = this.el.querySelector('[data-vz="file-input"]');
@@ -1450,6 +1511,7 @@ var Popover = class {
1450
1511
  this.position();
1451
1512
  }
1452
1513
  close() {
1514
+ this.closeMentionPicker();
1453
1515
  this.el.style.display = "none";
1454
1516
  this.anchorTargets = [];
1455
1517
  this.anchorFingerprints = [];
@@ -1507,7 +1569,7 @@ var Popover = class {
1507
1569
  <div class="vz-popover-actions">
1508
1570
  <button class="vz-btn vz-btn-ghost" data-vz="close">Close</button>
1509
1571
  ${this.callbacks.canUploadAttachments() ? `<button class="vz-btn vz-btn-ghost vz-btn-upload" data-vz="upload" title="Attach an image">+ image</button>` : ""}
1510
- ${this.currentUser ? `<button class="vz-btn vz-btn-ghost vz-btn-mention" data-vz="mention" title="Mention ${escapeHtml(this.currentUser.name)}">@ mention</button>` : ""}
1572
+ ${this.currentUser ? `<button class="vz-btn vz-btn-ghost vz-btn-mention" data-vz="mention" title="Mention a teammate">@ mention</button>` : ""}
1511
1573
  <button class="vz-btn vz-btn-primary" data-vz="save">Save</button>
1512
1574
  </div>
1513
1575
  <input type="file" class="vz-file-input" data-vz="file-input" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml" hidden />
@@ -1518,6 +1580,7 @@ var Popover = class {
1518
1580
  const editor = this.el.querySelector(".vz-textarea");
1519
1581
  editor.focus();
1520
1582
  editor.addEventListener("keydown", this.handleEditorKey);
1583
+ editor.addEventListener("input", this.handleEditorInput);
1521
1584
  editor.addEventListener("paste", this.handleEditorPaste);
1522
1585
  const wrap = this.el.querySelector(".vz-editor-wrap");
1523
1586
  if (wrap && this.callbacks.canUploadAttachments()) {
@@ -1547,6 +1610,37 @@ var Popover = class {
1547
1610
  <div class="vz-anchor-list">${chips}</div>
1548
1611
  `;
1549
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
+ }
1550
1644
  async uploadAndAppend(file) {
1551
1645
  if (!this.callbacks.canUploadAttachments()) return;
1552
1646
  const placeholderId = "upload-" + Math.random().toString(36).slice(2, 9);
@@ -1676,11 +1770,254 @@ var Popover = class {
1676
1770
  for (const child of editor.childNodes) walk(child);
1677
1771
  return parts.join("").trim();
1678
1772
  }
1773
+ /**
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).
1779
+ */
1780
+ async openMentionPicker(anchor) {
1781
+ this.closeMentionPicker();
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 } = {}) {
1827
+ const picker = document.createElement("div");
1828
+ picker.className = "vz-mention-picker";
1829
+ picker.setAttribute("data-vz-ignore", "");
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
+ `;
1836
+ this.el.appendChild(picker);
1837
+ this.mentionPicker = picker;
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 [];
1886
+ try {
1887
+ return await this.callbacks.onMentionSearch();
1888
+ } catch {
1889
+ return [];
1890
+ }
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>`;
1907
+ return;
1908
+ }
1909
+ list.innerHTML = this.mentionPickerFiltered.map((u, i) => {
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>`;
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>`;
1912
+ }).join("");
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();
1963
+ }
1964
+ /** Wires Esc + outside-click handlers to close the open picker. */
1965
+ wireMentionPickerDismiss() {
1966
+ if (this.mentionPickerDismiss) return;
1967
+ const onKey = (e) => {
1968
+ if (e.key === "Escape") {
1969
+ e.preventDefault();
1970
+ this.closeMentionPicker();
1971
+ }
1972
+ };
1973
+ const onOutside = (e) => {
1974
+ if (!this.mentionPicker) return;
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
+ }
1980
+ this.closeMentionPicker();
1981
+ };
1982
+ document.addEventListener("keydown", onKey, true);
1983
+ document.addEventListener("mousedown", onOutside, true);
1984
+ this.mentionPickerDismiss = () => {
1985
+ document.removeEventListener("keydown", onKey, true);
1986
+ document.removeEventListener("mousedown", onOutside, true);
1987
+ };
1988
+ }
1989
+ closeMentionPicker() {
1990
+ if (this.mentionPickerDismiss) {
1991
+ this.mentionPickerDismiss();
1992
+ this.mentionPickerDismiss = null;
1993
+ }
1994
+ if (this.mentionPicker) {
1995
+ this.mentionPicker.remove();
1996
+ this.mentionPicker = null;
1997
+ }
1998
+ this.mentionPickerFiltered = [];
1999
+ this.mentionPickerSelected = 0;
2000
+ this.mentionPickerEditorRange = null;
2001
+ }
2002
+ /** Position the picker below the given anchor (the @ mention button). */
2003
+ positionMentionPickerByElement(picker, anchor) {
2004
+ const popRect = this.el.getBoundingClientRect();
2005
+ const btnRect = anchor.getBoundingClientRect();
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`;
2015
+ }
1679
2016
  /**
1680
2017
  * Insert a mention chip for the given user into the editor at the
1681
- * current caret position. Owner-only ACL in v1: the only callable
1682
- * "mentionable" user is the current Vizu user (= workspace owner).
1683
- * Future versions can expand this with an autocomplete dropdown.
2018
+ * current caret position. Accepts any user-like shape with a required
2019
+ * `id` + `name` — covers both the current VizuUser (for self-mention)
2020
+ * and MentionableUser (from the workspace member picker).
1684
2021
  */
1685
2022
  insertMentionChip(user) {
1686
2023
  if (!user || !user.id) return;
@@ -1757,6 +2094,7 @@ var Popover = class {
1757
2094
  this.el.style.left = left + "px";
1758
2095
  }
1759
2096
  destroy() {
2097
+ this.closeMentionPicker();
1760
2098
  this.el.removeEventListener("click", this.handleClick);
1761
2099
  this.el.remove();
1762
2100
  }
@@ -2535,6 +2873,91 @@ var STYLES = `
2535
2873
  }
2536
2874
  .vz-btn-primary:hover { background: color-mix(in srgb, var(--vz-accent) 88%, white); }
2537
2875
 
2876
+ /* Mention picker \u2014 floating dropdown anchored under the @ mention button */
2877
+ .vz-mention-picker {
2878
+ position: absolute;
2879
+ z-index: 10;
2880
+ min-width: 200px;
2881
+ max-width: 280px;
2882
+ max-height: 240px;
2883
+ overflow-y: auto;
2884
+ padding: 4px;
2885
+ background: var(--vz-bg-popover, #1c1c1c);
2886
+ border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
2887
+ border-radius: 8px;
2888
+ box-shadow: 0 12px 28px rgba(0,0,0,0.4), 0 2px 6px rgba(0,0,0,0.3);
2889
+ display: flex;
2890
+ flex-direction: column;
2891
+ gap: 1px;
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
+ }
2913
+ .vz-mention-loading,
2914
+ .vz-mention-empty {
2915
+ padding: 10px 12px;
2916
+ font-size: 12px;
2917
+ color: var(--vz-text-muted);
2918
+ text-align: center;
2919
+ }
2920
+ .vz-mention-item {
2921
+ display: flex;
2922
+ align-items: center;
2923
+ gap: 8px;
2924
+ padding: 7px 10px;
2925
+ border-radius: 6px;
2926
+ background: transparent;
2927
+ border: none;
2928
+ color: #fafafa;
2929
+ font-size: 13px;
2930
+ text-align: left;
2931
+ cursor: pointer;
2932
+ transition: background 80ms;
2933
+ }
2934
+ .vz-mention-item.is-selected,
2935
+ .vz-mention-item:focus-visible {
2936
+ background: var(--vz-bg-hover, rgba(255,255,255,0.08));
2937
+ outline: none;
2938
+ }
2939
+ .vz-mention-avatar {
2940
+ display: inline-flex;
2941
+ align-items: center;
2942
+ justify-content: center;
2943
+ width: 22px; height: 22px;
2944
+ border-radius: 50%;
2945
+ font-size: 10px; font-weight: 600;
2946
+ color: var(--vz-marker-fg, #0a0a0a);
2947
+ background: var(--vz-accent, #ff8b6e);
2948
+ flex-shrink: 0;
2949
+ overflow: hidden;
2950
+ }
2951
+ .vz-mention-avatar img {
2952
+ width: 100%; height: 100%; object-fit: cover;
2953
+ }
2954
+ .vz-mention-item-name {
2955
+ flex: 1;
2956
+ white-space: nowrap;
2957
+ overflow: hidden;
2958
+ text-overflow: ellipsis;
2959
+ }
2960
+
2538
2961
  /* Sidebar */
2539
2962
  .vz-sidebar {
2540
2963
  position: fixed;
@@ -3302,6 +3725,18 @@ var Vizu = class {
3302
3725
  getUser() {
3303
3726
  return this.user;
3304
3727
  }
3728
+ /**
3729
+ * Fetch the list of users who can be @-mentioned on the current
3730
+ * workspace. Used by the popover's mention dropdown; hosts can also
3731
+ * call this directly to build their own picker. Returns [] for
3732
+ * non-cloud adapters (no workspace, no member list to fetch from).
3733
+ */
3734
+ async searchMentionable() {
3735
+ if (this.storage instanceof CloudStorageAdapter) {
3736
+ return this.storage.searchMentionable();
3737
+ }
3738
+ return [];
3739
+ }
3305
3740
  /**
3306
3741
  * Whether the full Vizu UI (pill, highlighter, markers, popover) can
3307
3742
  * be mounted right now. Cloud-mode workspaces hold off until the
@@ -3657,7 +4092,8 @@ var Vizu = class {
3657
4092
  onClose: () => this.closePopover(),
3658
4093
  onStartReferencePick: (onPicked, onCancel) => this.startPicking(onPicked, onCancel),
3659
4094
  onJumpToReference: (commentId, refId2) => this.jumpToReference(commentId, refId2),
3660
- onAnchorsChanged: (fps) => this.syncActiveAnchorOutlines(fps)
4095
+ onAnchorsChanged: (fps) => this.syncActiveAnchorOutlines(fps),
4096
+ onMentionSearch: () => this.searchMentionable()
3661
4097
  });
3662
4098
  this.popover.setUser(this.user);
3663
4099
  this.sidebar = new Sidebar(this.root, {