@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/index.cjs CHANGED
@@ -1271,8 +1271,54 @@ var Popover = class {
1271
1271
  this.pendingRefs = {};
1272
1272
  /** Attachments uploaded during this session, persisted into `comment.attachments` on save. */
1273
1273
  this.pendingAttachments = [];
1274
+ /** Open mention picker element, if any. Held so the next openMentionPicker / close calls can tear it down. */
1275
+ this.mentionPicker = null;
1276
+ /** Dismiss handlers wired to document for the open picker; cleared in closeMentionPicker. */
1277
+ this.mentionPickerDismiss = null;
1278
+ /** All mentionable users fetched for the open picker — pre-filter list. */
1279
+ this.mentionPickerUsers = [];
1280
+ /** Currently visible subset of `mentionPickerUsers` after the user's filter. */
1281
+ this.mentionPickerFiltered = [];
1282
+ /** Highlighted index in `mentionPickerFiltered`. Up/Down arrows mutate this. */
1283
+ this.mentionPickerSelected = 0;
1284
+ /**
1285
+ * 'manual' = opened from the @ mention button (separate search input).
1286
+ * 'editor' = opened by typing `@` in the editor; filter is derived
1287
+ * from the text after `@` in the editor itself, no search input.
1288
+ */
1289
+ this.mentionPickerMode = "manual";
1290
+ /**
1291
+ * In editor mode: a Range covering `@<query>` in the editor's text.
1292
+ * Used to replace that span with the mention chip on commit, and to
1293
+ * recompute the bounding rect when re-positioning after typing.
1294
+ */
1295
+ this.mentionPickerEditorRange = null;
1274
1296
  this.handleEditorKey = (e) => {
1275
1297
  const editor = e.currentTarget;
1298
+ if (this.mentionPicker && this.mentionPickerMode === "editor") {
1299
+ if (e.key === "ArrowDown") {
1300
+ e.preventDefault();
1301
+ this.moveMentionPickerSelection(1);
1302
+ return;
1303
+ }
1304
+ if (e.key === "ArrowUp") {
1305
+ e.preventDefault();
1306
+ this.moveMentionPickerSelection(-1);
1307
+ return;
1308
+ }
1309
+ if (e.key === "Enter" || e.key === "Tab") {
1310
+ if (this.mentionPickerFiltered.length > 0) {
1311
+ e.preventDefault();
1312
+ this.commitMentionPickerSelection();
1313
+ return;
1314
+ }
1315
+ }
1316
+ if (e.key === "Escape") {
1317
+ e.preventDefault();
1318
+ this.closeMentionPicker();
1319
+ return;
1320
+ }
1321
+ }
1276
1322
  if (e.key === "#") {
1277
1323
  e.preventDefault();
1278
1324
  const savedRange = this.saveRange();
@@ -1295,6 +1341,21 @@ var Popover = class {
1295
1341
  this.callbacks.onClose();
1296
1342
  }
1297
1343
  };
1344
+ /**
1345
+ * Fires after every keystroke in the editor (post-insertion). Looks
1346
+ * for an `@<query>` pattern adjacent to the caret. If found, opens
1347
+ * (or refreshes) the mention picker in editor mode. If the trigger
1348
+ * disappears (user typed a space, backspaced over @, etc.), closes
1349
+ * the picker.
1350
+ */
1351
+ this.handleEditorInput = () => {
1352
+ const trigger = this.detectMentionTrigger();
1353
+ if (trigger) {
1354
+ void this.openOrUpdateEditorMentionPicker(trigger.range, trigger.query);
1355
+ } else if (this.mentionPicker && this.mentionPickerMode === "editor") {
1356
+ this.closeMentionPicker();
1357
+ }
1358
+ };
1298
1359
  this.handleEditorPaste = (e) => {
1299
1360
  if (this.callbacks.canUploadAttachments()) {
1300
1361
  const items = e.clipboardData?.items ?? null;
@@ -1377,7 +1438,7 @@ var Popover = class {
1377
1438
  else if (action === "save") this.save();
1378
1439
  else if (action === "mention") {
1379
1440
  e.preventDefault();
1380
- this.insertMentionChip(this.currentUser);
1441
+ this.openMentionPicker(target);
1381
1442
  } else if (action === "upload") {
1382
1443
  e.preventDefault();
1383
1444
  const input = this.el.querySelector('[data-vz="file-input"]');
@@ -1486,6 +1547,7 @@ var Popover = class {
1486
1547
  this.position();
1487
1548
  }
1488
1549
  close() {
1550
+ this.closeMentionPicker();
1489
1551
  this.el.style.display = "none";
1490
1552
  this.anchorTargets = [];
1491
1553
  this.anchorFingerprints = [];
@@ -1543,7 +1605,7 @@ var Popover = class {
1543
1605
  <div class="vz-popover-actions">
1544
1606
  <button class="vz-btn vz-btn-ghost" data-vz="close">Close</button>
1545
1607
  ${this.callbacks.canUploadAttachments() ? `<button class="vz-btn vz-btn-ghost vz-btn-upload" data-vz="upload" title="Attach an image">+ image</button>` : ""}
1546
- ${this.currentUser ? `<button class="vz-btn vz-btn-ghost vz-btn-mention" data-vz="mention" title="Mention ${escapeHtml(this.currentUser.name)}">@ mention</button>` : ""}
1608
+ ${this.currentUser ? `<button class="vz-btn vz-btn-ghost vz-btn-mention" data-vz="mention" title="Mention a teammate">@ mention</button>` : ""}
1547
1609
  <button class="vz-btn vz-btn-primary" data-vz="save">Save</button>
1548
1610
  </div>
1549
1611
  <input type="file" class="vz-file-input" data-vz="file-input" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml" hidden />
@@ -1554,6 +1616,7 @@ var Popover = class {
1554
1616
  const editor = this.el.querySelector(".vz-textarea");
1555
1617
  editor.focus();
1556
1618
  editor.addEventListener("keydown", this.handleEditorKey);
1619
+ editor.addEventListener("input", this.handleEditorInput);
1557
1620
  editor.addEventListener("paste", this.handleEditorPaste);
1558
1621
  const wrap = this.el.querySelector(".vz-editor-wrap");
1559
1622
  if (wrap && this.callbacks.canUploadAttachments()) {
@@ -1583,6 +1646,37 @@ var Popover = class {
1583
1646
  <div class="vz-anchor-list">${chips}</div>
1584
1647
  `;
1585
1648
  }
1649
+ /**
1650
+ * Walk back from the caret looking for the most recent `@` that
1651
+ * starts a mention trigger — must be at start-of-text-node or
1652
+ * preceded by whitespace, and there must be no whitespace between
1653
+ * `@` and the caret. Returns the Range covering `@<query>` plus
1654
+ * the bare query string.
1655
+ */
1656
+ detectMentionTrigger() {
1657
+ const sel = window.getSelection();
1658
+ if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null;
1659
+ const caret = sel.getRangeAt(0);
1660
+ const node = caret.startContainer;
1661
+ if (node.nodeType !== Node.TEXT_NODE) return null;
1662
+ const text = node.textContent ?? "";
1663
+ const offset = caret.startOffset;
1664
+ for (let i = offset - 1; i >= 0; i--) {
1665
+ const ch = text[i];
1666
+ if (ch === "@") {
1667
+ const prev = i > 0 ? text[i - 1] : "";
1668
+ if (prev !== "" && !/\s/.test(prev)) return null;
1669
+ const query = text.slice(i + 1, offset);
1670
+ if (/\s/.test(query)) return null;
1671
+ const range = document.createRange();
1672
+ range.setStart(node, i);
1673
+ range.setEnd(node, offset);
1674
+ return { range, query };
1675
+ }
1676
+ if (/\s/.test(ch)) return null;
1677
+ }
1678
+ return null;
1679
+ }
1586
1680
  async uploadAndAppend(file) {
1587
1681
  if (!this.callbacks.canUploadAttachments()) return;
1588
1682
  const placeholderId = "upload-" + Math.random().toString(36).slice(2, 9);
@@ -1712,11 +1806,254 @@ var Popover = class {
1712
1806
  for (const child of editor.childNodes) walk(child);
1713
1807
  return parts.join("").trim();
1714
1808
  }
1809
+ /**
1810
+ * Manual-mode picker: opens from the @ mention button. Renders a
1811
+ * search input above the list; the user types into the input to
1812
+ * filter, arrows + Enter to pick. The selection inserts a mention
1813
+ * chip at the editor's current caret (no `@<query>` replacement —
1814
+ * the user didn't type one).
1815
+ */
1816
+ async openMentionPicker(anchor) {
1817
+ this.closeMentionPicker();
1818
+ if (!this.callbacks.onMentionSearch) return;
1819
+ this.mentionPickerMode = "manual";
1820
+ this.mentionPickerEditorRange = null;
1821
+ const picker = this.createMentionPickerShell();
1822
+ this.positionMentionPickerByElement(picker, anchor);
1823
+ const users = await this.fetchMentionables();
1824
+ if (this.mentionPicker !== picker) return;
1825
+ this.mentionPickerUsers = users;
1826
+ this.renderMentionPickerBody("");
1827
+ this.positionMentionPickerByElement(picker, anchor);
1828
+ const input = picker.querySelector(".vz-mention-search");
1829
+ input?.focus();
1830
+ this.wireMentionPickerDismiss();
1831
+ }
1832
+ /**
1833
+ * Editor-mode picker: opens because the user typed `@` in the
1834
+ * editor. No search input — the editor IS the search; the query is
1835
+ * the text after `@`. On commit, replaces the `@<query>` range with
1836
+ * the chip.
1837
+ */
1838
+ async openOrUpdateEditorMentionPicker(range, query) {
1839
+ this.mentionPickerEditorRange = range;
1840
+ if (!this.mentionPicker || this.mentionPickerMode !== "editor") {
1841
+ this.closeMentionPicker();
1842
+ this.mentionPickerMode = "editor";
1843
+ const picker = this.createMentionPickerShell({ showSearch: false });
1844
+ this.positionMentionPickerByRange(picker, range);
1845
+ const users = await this.fetchMentionables();
1846
+ if (this.mentionPicker !== picker) return;
1847
+ this.mentionPickerUsers = users;
1848
+ this.renderMentionPickerBody(query);
1849
+ this.positionMentionPickerByRange(picker, range);
1850
+ this.wireMentionPickerDismiss();
1851
+ return;
1852
+ }
1853
+ this.renderMentionPickerBody(query);
1854
+ this.positionMentionPickerByRange(this.mentionPicker, range);
1855
+ }
1856
+ /**
1857
+ * Build the picker DOM scaffold and attach it to the popover. Does
1858
+ * NOT fetch users or render the body — caller's responsibility.
1859
+ * Search input is hidden in editor mode where the user already has
1860
+ * a perfectly good text-entry surface (the editor itself).
1861
+ */
1862
+ createMentionPickerShell({ showSearch = true } = {}) {
1863
+ const picker = document.createElement("div");
1864
+ picker.className = "vz-mention-picker";
1865
+ picker.setAttribute("data-vz-ignore", "");
1866
+ picker.innerHTML = `
1867
+ ${showSearch ? `<input class="vz-mention-search" type="text" placeholder="Search teammates\u2026" autocomplete="off" spellcheck="false" />` : ""}
1868
+ <div class="vz-mention-list" data-vz="mention-list">
1869
+ <div class="vz-mention-loading">Loading\u2026</div>
1870
+ </div>
1871
+ `;
1872
+ this.el.appendChild(picker);
1873
+ this.mentionPicker = picker;
1874
+ this.mentionPickerFiltered = [];
1875
+ this.mentionPickerSelected = 0;
1876
+ picker.addEventListener("mousemove", (e) => {
1877
+ const btn = e.target?.closest(".vz-mention-item");
1878
+ if (!btn) return;
1879
+ const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
1880
+ if (idx >= 0 && idx !== this.mentionPickerSelected) {
1881
+ this.mentionPickerSelected = idx;
1882
+ this.refreshMentionPickerHighlight();
1883
+ }
1884
+ });
1885
+ picker.addEventListener("click", (e) => {
1886
+ const btn = e.target?.closest(".vz-mention-item");
1887
+ if (!btn) return;
1888
+ e.preventDefault();
1889
+ e.stopPropagation();
1890
+ const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
1891
+ if (idx >= 0) {
1892
+ this.mentionPickerSelected = idx;
1893
+ this.commitMentionPickerSelection();
1894
+ }
1895
+ });
1896
+ if (showSearch) {
1897
+ const input = picker.querySelector(".vz-mention-search");
1898
+ input?.addEventListener("input", () => this.renderMentionPickerBody(input.value));
1899
+ input?.addEventListener("keydown", (e) => {
1900
+ if (e.key === "ArrowDown") {
1901
+ e.preventDefault();
1902
+ this.moveMentionPickerSelection(1);
1903
+ } else if (e.key === "ArrowUp") {
1904
+ e.preventDefault();
1905
+ this.moveMentionPickerSelection(-1);
1906
+ } else if (e.key === "Enter" || e.key === "Tab") {
1907
+ if (this.mentionPickerFiltered.length > 0) {
1908
+ e.preventDefault();
1909
+ this.commitMentionPickerSelection();
1910
+ }
1911
+ } else if (e.key === "Escape") {
1912
+ e.preventDefault();
1913
+ this.closeMentionPicker();
1914
+ }
1915
+ });
1916
+ }
1917
+ return picker;
1918
+ }
1919
+ /** Fetch the mentionable list. Swallows errors → []. */
1920
+ async fetchMentionables() {
1921
+ if (!this.callbacks.onMentionSearch) return [];
1922
+ try {
1923
+ return await this.callbacks.onMentionSearch();
1924
+ } catch {
1925
+ return [];
1926
+ }
1927
+ }
1928
+ /**
1929
+ * Re-filter the cached users by `query`, render the list, reset
1930
+ * selected to 0. Called on every search input change and on every
1931
+ * editor input event while in editor mode.
1932
+ */
1933
+ renderMentionPickerBody(query) {
1934
+ const picker = this.mentionPicker;
1935
+ if (!picker) return;
1936
+ const list = picker.querySelector('[data-vz="mention-list"]');
1937
+ if (!list) return;
1938
+ const q = query.toLowerCase().trim();
1939
+ this.mentionPickerFiltered = q ? this.mentionPickerUsers.filter((u) => u.name.toLowerCase().includes(q)) : this.mentionPickerUsers.slice();
1940
+ this.mentionPickerSelected = 0;
1941
+ if (this.mentionPickerFiltered.length === 0) {
1942
+ list.innerHTML = `<div class="vz-mention-empty">${this.mentionPickerUsers.length === 0 ? "No teammates to mention yet." : "No teammates match."}</div>`;
1943
+ return;
1944
+ }
1945
+ list.innerHTML = this.mentionPickerFiltered.map((u, i) => {
1946
+ 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>`;
1947
+ 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>`;
1948
+ }).join("");
1949
+ }
1950
+ moveMentionPickerSelection(delta) {
1951
+ if (this.mentionPickerFiltered.length === 0) return;
1952
+ const n = this.mentionPickerFiltered.length;
1953
+ this.mentionPickerSelected = (this.mentionPickerSelected + delta + n) % n;
1954
+ this.refreshMentionPickerHighlight();
1955
+ }
1956
+ refreshMentionPickerHighlight() {
1957
+ const picker = this.mentionPicker;
1958
+ if (!picker) return;
1959
+ const items = picker.querySelectorAll(".vz-mention-item");
1960
+ items.forEach((el, i) => el.classList.toggle("is-selected", i === this.mentionPickerSelected));
1961
+ items[this.mentionPickerSelected]?.scrollIntoView({ block: "nearest" });
1962
+ }
1963
+ commitMentionPickerSelection() {
1964
+ const user = this.mentionPickerFiltered[this.mentionPickerSelected];
1965
+ if (!user) return;
1966
+ if (this.mentionPickerMode === "editor" && this.mentionPickerEditorRange) {
1967
+ this.replaceRangeWithMentionChip(this.mentionPickerEditorRange, user);
1968
+ } else {
1969
+ this.insertMentionChip(user);
1970
+ }
1971
+ this.closeMentionPicker();
1972
+ }
1973
+ /**
1974
+ * Swap a Range covering `@<query>` for a mention chip + trailing
1975
+ * space, then place the caret after the space so the user can keep
1976
+ * typing. Used only in editor-trigger mode.
1977
+ */
1978
+ replaceRangeWithMentionChip(range, user) {
1979
+ const editor = this.el.querySelector(".vz-textarea");
1980
+ if (!editor) return;
1981
+ const chip = document.createElement("span");
1982
+ chip.className = "vz-chip vz-chip-mention";
1983
+ chip.contentEditable = "false";
1984
+ chip.setAttribute("data-mention-id", user.id);
1985
+ chip.textContent = "@" + user.name;
1986
+ range.deleteContents();
1987
+ range.insertNode(chip);
1988
+ const space = document.createTextNode(" ");
1989
+ chip.after(space);
1990
+ const sel = window.getSelection();
1991
+ if (sel) {
1992
+ const after = document.createRange();
1993
+ after.setStartAfter(space);
1994
+ after.collapse(true);
1995
+ sel.removeAllRanges();
1996
+ sel.addRange(after);
1997
+ }
1998
+ editor.focus();
1999
+ }
2000
+ /** Wires Esc + outside-click handlers to close the open picker. */
2001
+ wireMentionPickerDismiss() {
2002
+ if (this.mentionPickerDismiss) return;
2003
+ const onKey = (e) => {
2004
+ if (e.key === "Escape") {
2005
+ e.preventDefault();
2006
+ this.closeMentionPicker();
2007
+ }
2008
+ };
2009
+ const onOutside = (e) => {
2010
+ if (!this.mentionPicker) return;
2011
+ if (this.mentionPicker.contains(e.target)) return;
2012
+ if (this.mentionPickerMode === "editor") {
2013
+ const editor = this.el.querySelector(".vz-textarea");
2014
+ if (editor && editor.contains(e.target)) return;
2015
+ }
2016
+ this.closeMentionPicker();
2017
+ };
2018
+ document.addEventListener("keydown", onKey, true);
2019
+ document.addEventListener("mousedown", onOutside, true);
2020
+ this.mentionPickerDismiss = () => {
2021
+ document.removeEventListener("keydown", onKey, true);
2022
+ document.removeEventListener("mousedown", onOutside, true);
2023
+ };
2024
+ }
2025
+ closeMentionPicker() {
2026
+ if (this.mentionPickerDismiss) {
2027
+ this.mentionPickerDismiss();
2028
+ this.mentionPickerDismiss = null;
2029
+ }
2030
+ if (this.mentionPicker) {
2031
+ this.mentionPicker.remove();
2032
+ this.mentionPicker = null;
2033
+ }
2034
+ this.mentionPickerFiltered = [];
2035
+ this.mentionPickerSelected = 0;
2036
+ this.mentionPickerEditorRange = null;
2037
+ }
2038
+ /** Position the picker below the given anchor (the @ mention button). */
2039
+ positionMentionPickerByElement(picker, anchor) {
2040
+ const popRect = this.el.getBoundingClientRect();
2041
+ const btnRect = anchor.getBoundingClientRect();
2042
+ picker.style.left = `${btnRect.left - popRect.left}px`;
2043
+ picker.style.top = `${btnRect.bottom - popRect.top + 4}px`;
2044
+ }
2045
+ /** Position the picker below the `@<query>` text range in the editor. */
2046
+ positionMentionPickerByRange(picker, range) {
2047
+ const popRect = this.el.getBoundingClientRect();
2048
+ const rect = range.getBoundingClientRect();
2049
+ picker.style.left = `${rect.left - popRect.left}px`;
2050
+ picker.style.top = `${rect.bottom - popRect.top + 4}px`;
2051
+ }
1715
2052
  /**
1716
2053
  * Insert a mention chip for the given user into the editor at the
1717
- * current caret position. Owner-only ACL in v1: the only callable
1718
- * "mentionable" user is the current Vizu user (= workspace owner).
1719
- * Future versions can expand this with an autocomplete dropdown.
2054
+ * current caret position. Accepts any user-like shape with a required
2055
+ * `id` + `name` — covers both the current VizuUser (for self-mention)
2056
+ * and MentionableUser (from the workspace member picker).
1720
2057
  */
1721
2058
  insertMentionChip(user) {
1722
2059
  if (!user || !user.id) return;
@@ -1793,6 +2130,7 @@ var Popover = class {
1793
2130
  this.el.style.left = left + "px";
1794
2131
  }
1795
2132
  destroy() {
2133
+ this.closeMentionPicker();
1796
2134
  this.el.removeEventListener("click", this.handleClick);
1797
2135
  this.el.remove();
1798
2136
  }
@@ -2571,6 +2909,91 @@ var STYLES = `
2571
2909
  }
2572
2910
  .vz-btn-primary:hover { background: color-mix(in srgb, var(--vz-accent) 88%, white); }
2573
2911
 
2912
+ /* Mention picker \u2014 floating dropdown anchored under the @ mention button */
2913
+ .vz-mention-picker {
2914
+ position: absolute;
2915
+ z-index: 10;
2916
+ min-width: 200px;
2917
+ max-width: 280px;
2918
+ max-height: 240px;
2919
+ overflow-y: auto;
2920
+ padding: 4px;
2921
+ background: var(--vz-bg-popover, #1c1c1c);
2922
+ border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
2923
+ border-radius: 8px;
2924
+ box-shadow: 0 12px 28px rgba(0,0,0,0.4), 0 2px 6px rgba(0,0,0,0.3);
2925
+ display: flex;
2926
+ flex-direction: column;
2927
+ gap: 1px;
2928
+ }
2929
+ .vz-mention-search {
2930
+ width: 100%;
2931
+ padding: 7px 9px;
2932
+ margin-bottom: 4px;
2933
+ background: rgba(255,255,255,0.04);
2934
+ border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
2935
+ border-radius: 6px;
2936
+ color: #fafafa;
2937
+ font-size: 13px;
2938
+ outline: none;
2939
+ box-sizing: border-box;
2940
+ }
2941
+ .vz-mention-search:focus { border-color: var(--vz-accent, #ff8b6e); }
2942
+ .vz-mention-search::placeholder { color: var(--vz-text-muted); }
2943
+ .vz-mention-list {
2944
+ display: flex;
2945
+ flex-direction: column;
2946
+ gap: 1px;
2947
+ min-height: 0;
2948
+ }
2949
+ .vz-mention-loading,
2950
+ .vz-mention-empty {
2951
+ padding: 10px 12px;
2952
+ font-size: 12px;
2953
+ color: var(--vz-text-muted);
2954
+ text-align: center;
2955
+ }
2956
+ .vz-mention-item {
2957
+ display: flex;
2958
+ align-items: center;
2959
+ gap: 8px;
2960
+ padding: 7px 10px;
2961
+ border-radius: 6px;
2962
+ background: transparent;
2963
+ border: none;
2964
+ color: #fafafa;
2965
+ font-size: 13px;
2966
+ text-align: left;
2967
+ cursor: pointer;
2968
+ transition: background 80ms;
2969
+ }
2970
+ .vz-mention-item.is-selected,
2971
+ .vz-mention-item:focus-visible {
2972
+ background: var(--vz-bg-hover, rgba(255,255,255,0.08));
2973
+ outline: none;
2974
+ }
2975
+ .vz-mention-avatar {
2976
+ display: inline-flex;
2977
+ align-items: center;
2978
+ justify-content: center;
2979
+ width: 22px; height: 22px;
2980
+ border-radius: 50%;
2981
+ font-size: 10px; font-weight: 600;
2982
+ color: var(--vz-marker-fg, #0a0a0a);
2983
+ background: var(--vz-accent, #ff8b6e);
2984
+ flex-shrink: 0;
2985
+ overflow: hidden;
2986
+ }
2987
+ .vz-mention-avatar img {
2988
+ width: 100%; height: 100%; object-fit: cover;
2989
+ }
2990
+ .vz-mention-item-name {
2991
+ flex: 1;
2992
+ white-space: nowrap;
2993
+ overflow: hidden;
2994
+ text-overflow: ellipsis;
2995
+ }
2996
+
2574
2997
  /* Sidebar */
2575
2998
  .vz-sidebar {
2576
2999
  position: fixed;
@@ -3338,6 +3761,18 @@ var Vizu = class {
3338
3761
  getUser() {
3339
3762
  return this.user;
3340
3763
  }
3764
+ /**
3765
+ * Fetch the list of users who can be @-mentioned on the current
3766
+ * workspace. Used by the popover's mention dropdown; hosts can also
3767
+ * call this directly to build their own picker. Returns [] for
3768
+ * non-cloud adapters (no workspace, no member list to fetch from).
3769
+ */
3770
+ async searchMentionable() {
3771
+ if (this.storage instanceof CloudStorageAdapter) {
3772
+ return this.storage.searchMentionable();
3773
+ }
3774
+ return [];
3775
+ }
3341
3776
  /**
3342
3777
  * Whether the full Vizu UI (pill, highlighter, markers, popover) can
3343
3778
  * be mounted right now. Cloud-mode workspaces hold off until the
@@ -3693,7 +4128,8 @@ var Vizu = class {
3693
4128
  onClose: () => this.closePopover(),
3694
4129
  onStartReferencePick: (onPicked, onCancel) => this.startPicking(onPicked, onCancel),
3695
4130
  onJumpToReference: (commentId, refId2) => this.jumpToReference(commentId, refId2),
3696
- onAnchorsChanged: (fps) => this.syncActiveAnchorOutlines(fps)
4131
+ onAnchorsChanged: (fps) => this.syncActiveAnchorOutlines(fps),
4132
+ onMentionSearch: () => this.searchMentionable()
3697
4133
  });
3698
4134
  this.popover.setUser(this.user);
3699
4135
  this.sidebar = new Sidebar(this.root, {