@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.cjs CHANGED
@@ -1261,8 +1261,54 @@ var Popover = class {
1261
1261
  this.pendingRefs = {};
1262
1262
  /** Attachments uploaded during this session, persisted into `comment.attachments` on save. */
1263
1263
  this.pendingAttachments = [];
1264
+ /** Open mention picker element, if any. Held so the next openMentionPicker / close calls can tear it down. */
1265
+ this.mentionPicker = null;
1266
+ /** Dismiss handlers wired to document for the open picker; cleared in closeMentionPicker. */
1267
+ this.mentionPickerDismiss = null;
1268
+ /** All mentionable users fetched for the open picker — pre-filter list. */
1269
+ this.mentionPickerUsers = [];
1270
+ /** Currently visible subset of `mentionPickerUsers` after the user's filter. */
1271
+ this.mentionPickerFiltered = [];
1272
+ /** Highlighted index in `mentionPickerFiltered`. Up/Down arrows mutate this. */
1273
+ this.mentionPickerSelected = 0;
1274
+ /**
1275
+ * 'manual' = opened from the @ mention button (separate search input).
1276
+ * 'editor' = opened by typing `@` in the editor; filter is derived
1277
+ * from the text after `@` in the editor itself, no search input.
1278
+ */
1279
+ this.mentionPickerMode = "manual";
1280
+ /**
1281
+ * In editor mode: a Range covering `@<query>` in the editor's text.
1282
+ * Used to replace that span with the mention chip on commit, and to
1283
+ * recompute the bounding rect when re-positioning after typing.
1284
+ */
1285
+ this.mentionPickerEditorRange = null;
1264
1286
  this.handleEditorKey = (e) => {
1265
1287
  const editor = e.currentTarget;
1288
+ if (this.mentionPicker && this.mentionPickerMode === "editor") {
1289
+ if (e.key === "ArrowDown") {
1290
+ e.preventDefault();
1291
+ this.moveMentionPickerSelection(1);
1292
+ return;
1293
+ }
1294
+ if (e.key === "ArrowUp") {
1295
+ e.preventDefault();
1296
+ this.moveMentionPickerSelection(-1);
1297
+ return;
1298
+ }
1299
+ if (e.key === "Enter" || e.key === "Tab") {
1300
+ if (this.mentionPickerFiltered.length > 0) {
1301
+ e.preventDefault();
1302
+ this.commitMentionPickerSelection();
1303
+ return;
1304
+ }
1305
+ }
1306
+ if (e.key === "Escape") {
1307
+ e.preventDefault();
1308
+ this.closeMentionPicker();
1309
+ return;
1310
+ }
1311
+ }
1266
1312
  if (e.key === "#") {
1267
1313
  e.preventDefault();
1268
1314
  const savedRange = this.saveRange();
@@ -1285,6 +1331,21 @@ var Popover = class {
1285
1331
  this.callbacks.onClose();
1286
1332
  }
1287
1333
  };
1334
+ /**
1335
+ * Fires after every keystroke in the editor (post-insertion). Looks
1336
+ * for an `@<query>` pattern adjacent to the caret. If found, opens
1337
+ * (or refreshes) the mention picker in editor mode. If the trigger
1338
+ * disappears (user typed a space, backspaced over @, etc.), closes
1339
+ * the picker.
1340
+ */
1341
+ this.handleEditorInput = () => {
1342
+ const trigger = this.detectMentionTrigger();
1343
+ if (trigger) {
1344
+ void this.openOrUpdateEditorMentionPicker(trigger.range, trigger.query);
1345
+ } else if (this.mentionPicker && this.mentionPickerMode === "editor") {
1346
+ this.closeMentionPicker();
1347
+ }
1348
+ };
1288
1349
  this.handleEditorPaste = (e) => {
1289
1350
  if (this.callbacks.canUploadAttachments()) {
1290
1351
  const items = e.clipboardData?.items ?? null;
@@ -1367,7 +1428,7 @@ var Popover = class {
1367
1428
  else if (action === "save") this.save();
1368
1429
  else if (action === "mention") {
1369
1430
  e.preventDefault();
1370
- this.insertMentionChip(this.currentUser);
1431
+ this.openMentionPicker(target);
1371
1432
  } else if (action === "upload") {
1372
1433
  e.preventDefault();
1373
1434
  const input = this.el.querySelector('[data-vz="file-input"]');
@@ -1476,6 +1537,7 @@ var Popover = class {
1476
1537
  this.position();
1477
1538
  }
1478
1539
  close() {
1540
+ this.closeMentionPicker();
1479
1541
  this.el.style.display = "none";
1480
1542
  this.anchorTargets = [];
1481
1543
  this.anchorFingerprints = [];
@@ -1533,7 +1595,7 @@ var Popover = class {
1533
1595
  <div class="vz-popover-actions">
1534
1596
  <button class="vz-btn vz-btn-ghost" data-vz="close">Close</button>
1535
1597
  ${this.callbacks.canUploadAttachments() ? `<button class="vz-btn vz-btn-ghost vz-btn-upload" data-vz="upload" title="Attach an image">+ image</button>` : ""}
1536
- ${this.currentUser ? `<button class="vz-btn vz-btn-ghost vz-btn-mention" data-vz="mention" title="Mention ${escapeHtml(this.currentUser.name)}">@ mention</button>` : ""}
1598
+ ${this.currentUser ? `<button class="vz-btn vz-btn-ghost vz-btn-mention" data-vz="mention" title="Mention a teammate">@ mention</button>` : ""}
1537
1599
  <button class="vz-btn vz-btn-primary" data-vz="save">Save</button>
1538
1600
  </div>
1539
1601
  <input type="file" class="vz-file-input" data-vz="file-input" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml" hidden />
@@ -1544,6 +1606,7 @@ var Popover = class {
1544
1606
  const editor = this.el.querySelector(".vz-textarea");
1545
1607
  editor.focus();
1546
1608
  editor.addEventListener("keydown", this.handleEditorKey);
1609
+ editor.addEventListener("input", this.handleEditorInput);
1547
1610
  editor.addEventListener("paste", this.handleEditorPaste);
1548
1611
  const wrap = this.el.querySelector(".vz-editor-wrap");
1549
1612
  if (wrap && this.callbacks.canUploadAttachments()) {
@@ -1573,6 +1636,37 @@ var Popover = class {
1573
1636
  <div class="vz-anchor-list">${chips}</div>
1574
1637
  `;
1575
1638
  }
1639
+ /**
1640
+ * Walk back from the caret looking for the most recent `@` that
1641
+ * starts a mention trigger — must be at start-of-text-node or
1642
+ * preceded by whitespace, and there must be no whitespace between
1643
+ * `@` and the caret. Returns the Range covering `@<query>` plus
1644
+ * the bare query string.
1645
+ */
1646
+ detectMentionTrigger() {
1647
+ const sel = window.getSelection();
1648
+ if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null;
1649
+ const caret = sel.getRangeAt(0);
1650
+ const node = caret.startContainer;
1651
+ if (node.nodeType !== Node.TEXT_NODE) return null;
1652
+ const text = node.textContent ?? "";
1653
+ const offset = caret.startOffset;
1654
+ for (let i = offset - 1; i >= 0; i--) {
1655
+ const ch = text[i];
1656
+ if (ch === "@") {
1657
+ const prev = i > 0 ? text[i - 1] : "";
1658
+ if (prev !== "" && !/\s/.test(prev)) return null;
1659
+ const query = text.slice(i + 1, offset);
1660
+ if (/\s/.test(query)) return null;
1661
+ const range = document.createRange();
1662
+ range.setStart(node, i);
1663
+ range.setEnd(node, offset);
1664
+ return { range, query };
1665
+ }
1666
+ if (/\s/.test(ch)) return null;
1667
+ }
1668
+ return null;
1669
+ }
1576
1670
  async uploadAndAppend(file) {
1577
1671
  if (!this.callbacks.canUploadAttachments()) return;
1578
1672
  const placeholderId = "upload-" + Math.random().toString(36).slice(2, 9);
@@ -1702,11 +1796,254 @@ var Popover = class {
1702
1796
  for (const child of editor.childNodes) walk(child);
1703
1797
  return parts.join("").trim();
1704
1798
  }
1799
+ /**
1800
+ * Manual-mode picker: opens from the @ mention button. Renders a
1801
+ * search input above the list; the user types into the input to
1802
+ * filter, arrows + Enter to pick. The selection inserts a mention
1803
+ * chip at the editor's current caret (no `@<query>` replacement —
1804
+ * the user didn't type one).
1805
+ */
1806
+ async openMentionPicker(anchor) {
1807
+ this.closeMentionPicker();
1808
+ if (!this.callbacks.onMentionSearch) return;
1809
+ this.mentionPickerMode = "manual";
1810
+ this.mentionPickerEditorRange = null;
1811
+ const picker = this.createMentionPickerShell();
1812
+ this.positionMentionPickerByElement(picker, anchor);
1813
+ const users = await this.fetchMentionables();
1814
+ if (this.mentionPicker !== picker) return;
1815
+ this.mentionPickerUsers = users;
1816
+ this.renderMentionPickerBody("");
1817
+ this.positionMentionPickerByElement(picker, anchor);
1818
+ const input = picker.querySelector(".vz-mention-search");
1819
+ input?.focus();
1820
+ this.wireMentionPickerDismiss();
1821
+ }
1822
+ /**
1823
+ * Editor-mode picker: opens because the user typed `@` in the
1824
+ * editor. No search input — the editor IS the search; the query is
1825
+ * the text after `@`. On commit, replaces the `@<query>` range with
1826
+ * the chip.
1827
+ */
1828
+ async openOrUpdateEditorMentionPicker(range, query) {
1829
+ this.mentionPickerEditorRange = range;
1830
+ if (!this.mentionPicker || this.mentionPickerMode !== "editor") {
1831
+ this.closeMentionPicker();
1832
+ this.mentionPickerMode = "editor";
1833
+ const picker = this.createMentionPickerShell({ showSearch: false });
1834
+ this.positionMentionPickerByRange(picker, range);
1835
+ const users = await this.fetchMentionables();
1836
+ if (this.mentionPicker !== picker) return;
1837
+ this.mentionPickerUsers = users;
1838
+ this.renderMentionPickerBody(query);
1839
+ this.positionMentionPickerByRange(picker, range);
1840
+ this.wireMentionPickerDismiss();
1841
+ return;
1842
+ }
1843
+ this.renderMentionPickerBody(query);
1844
+ this.positionMentionPickerByRange(this.mentionPicker, range);
1845
+ }
1846
+ /**
1847
+ * Build the picker DOM scaffold and attach it to the popover. Does
1848
+ * NOT fetch users or render the body — caller's responsibility.
1849
+ * Search input is hidden in editor mode where the user already has
1850
+ * a perfectly good text-entry surface (the editor itself).
1851
+ */
1852
+ createMentionPickerShell({ showSearch = true } = {}) {
1853
+ const picker = document.createElement("div");
1854
+ picker.className = "vz-mention-picker";
1855
+ picker.setAttribute("data-vz-ignore", "");
1856
+ picker.innerHTML = `
1857
+ ${showSearch ? `<input class="vz-mention-search" type="text" placeholder="Search teammates\u2026" autocomplete="off" spellcheck="false" />` : ""}
1858
+ <div class="vz-mention-list" data-vz="mention-list">
1859
+ <div class="vz-mention-loading">Loading\u2026</div>
1860
+ </div>
1861
+ `;
1862
+ this.el.appendChild(picker);
1863
+ this.mentionPicker = picker;
1864
+ this.mentionPickerFiltered = [];
1865
+ this.mentionPickerSelected = 0;
1866
+ picker.addEventListener("mousemove", (e) => {
1867
+ const btn = e.target?.closest(".vz-mention-item");
1868
+ if (!btn) return;
1869
+ const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
1870
+ if (idx >= 0 && idx !== this.mentionPickerSelected) {
1871
+ this.mentionPickerSelected = idx;
1872
+ this.refreshMentionPickerHighlight();
1873
+ }
1874
+ });
1875
+ picker.addEventListener("click", (e) => {
1876
+ const btn = e.target?.closest(".vz-mention-item");
1877
+ if (!btn) return;
1878
+ e.preventDefault();
1879
+ e.stopPropagation();
1880
+ const idx = Number(btn.getAttribute("data-mention-index") ?? -1);
1881
+ if (idx >= 0) {
1882
+ this.mentionPickerSelected = idx;
1883
+ this.commitMentionPickerSelection();
1884
+ }
1885
+ });
1886
+ if (showSearch) {
1887
+ const input = picker.querySelector(".vz-mention-search");
1888
+ input?.addEventListener("input", () => this.renderMentionPickerBody(input.value));
1889
+ input?.addEventListener("keydown", (e) => {
1890
+ if (e.key === "ArrowDown") {
1891
+ e.preventDefault();
1892
+ this.moveMentionPickerSelection(1);
1893
+ } else if (e.key === "ArrowUp") {
1894
+ e.preventDefault();
1895
+ this.moveMentionPickerSelection(-1);
1896
+ } else if (e.key === "Enter" || e.key === "Tab") {
1897
+ if (this.mentionPickerFiltered.length > 0) {
1898
+ e.preventDefault();
1899
+ this.commitMentionPickerSelection();
1900
+ }
1901
+ } else if (e.key === "Escape") {
1902
+ e.preventDefault();
1903
+ this.closeMentionPicker();
1904
+ }
1905
+ });
1906
+ }
1907
+ return picker;
1908
+ }
1909
+ /** Fetch the mentionable list. Swallows errors → []. */
1910
+ async fetchMentionables() {
1911
+ if (!this.callbacks.onMentionSearch) return [];
1912
+ try {
1913
+ return await this.callbacks.onMentionSearch();
1914
+ } catch {
1915
+ return [];
1916
+ }
1917
+ }
1918
+ /**
1919
+ * Re-filter the cached users by `query`, render the list, reset
1920
+ * selected to 0. Called on every search input change and on every
1921
+ * editor input event while in editor mode.
1922
+ */
1923
+ renderMentionPickerBody(query) {
1924
+ const picker = this.mentionPicker;
1925
+ if (!picker) return;
1926
+ const list = picker.querySelector('[data-vz="mention-list"]');
1927
+ if (!list) return;
1928
+ const q = query.toLowerCase().trim();
1929
+ this.mentionPickerFiltered = q ? this.mentionPickerUsers.filter((u) => u.name.toLowerCase().includes(q)) : this.mentionPickerUsers.slice();
1930
+ this.mentionPickerSelected = 0;
1931
+ if (this.mentionPickerFiltered.length === 0) {
1932
+ list.innerHTML = `<div class="vz-mention-empty">${this.mentionPickerUsers.length === 0 ? "No teammates to mention yet." : "No teammates match."}</div>`;
1933
+ return;
1934
+ }
1935
+ list.innerHTML = this.mentionPickerFiltered.map((u, i) => {
1936
+ 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>`;
1937
+ 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>`;
1938
+ }).join("");
1939
+ }
1940
+ moveMentionPickerSelection(delta) {
1941
+ if (this.mentionPickerFiltered.length === 0) return;
1942
+ const n = this.mentionPickerFiltered.length;
1943
+ this.mentionPickerSelected = (this.mentionPickerSelected + delta + n) % n;
1944
+ this.refreshMentionPickerHighlight();
1945
+ }
1946
+ refreshMentionPickerHighlight() {
1947
+ const picker = this.mentionPicker;
1948
+ if (!picker) return;
1949
+ const items = picker.querySelectorAll(".vz-mention-item");
1950
+ items.forEach((el, i) => el.classList.toggle("is-selected", i === this.mentionPickerSelected));
1951
+ items[this.mentionPickerSelected]?.scrollIntoView({ block: "nearest" });
1952
+ }
1953
+ commitMentionPickerSelection() {
1954
+ const user = this.mentionPickerFiltered[this.mentionPickerSelected];
1955
+ if (!user) return;
1956
+ if (this.mentionPickerMode === "editor" && this.mentionPickerEditorRange) {
1957
+ this.replaceRangeWithMentionChip(this.mentionPickerEditorRange, user);
1958
+ } else {
1959
+ this.insertMentionChip(user);
1960
+ }
1961
+ this.closeMentionPicker();
1962
+ }
1963
+ /**
1964
+ * Swap a Range covering `@<query>` for a mention chip + trailing
1965
+ * space, then place the caret after the space so the user can keep
1966
+ * typing. Used only in editor-trigger mode.
1967
+ */
1968
+ replaceRangeWithMentionChip(range, user) {
1969
+ const editor = this.el.querySelector(".vz-textarea");
1970
+ if (!editor) return;
1971
+ const chip = document.createElement("span");
1972
+ chip.className = "vz-chip vz-chip-mention";
1973
+ chip.contentEditable = "false";
1974
+ chip.setAttribute("data-mention-id", user.id);
1975
+ chip.textContent = "@" + user.name;
1976
+ range.deleteContents();
1977
+ range.insertNode(chip);
1978
+ const space = document.createTextNode(" ");
1979
+ chip.after(space);
1980
+ const sel = window.getSelection();
1981
+ if (sel) {
1982
+ const after = document.createRange();
1983
+ after.setStartAfter(space);
1984
+ after.collapse(true);
1985
+ sel.removeAllRanges();
1986
+ sel.addRange(after);
1987
+ }
1988
+ editor.focus();
1989
+ }
1990
+ /** Wires Esc + outside-click handlers to close the open picker. */
1991
+ wireMentionPickerDismiss() {
1992
+ if (this.mentionPickerDismiss) return;
1993
+ const onKey = (e) => {
1994
+ if (e.key === "Escape") {
1995
+ e.preventDefault();
1996
+ this.closeMentionPicker();
1997
+ }
1998
+ };
1999
+ const onOutside = (e) => {
2000
+ if (!this.mentionPicker) return;
2001
+ if (this.mentionPicker.contains(e.target)) return;
2002
+ if (this.mentionPickerMode === "editor") {
2003
+ const editor = this.el.querySelector(".vz-textarea");
2004
+ if (editor && editor.contains(e.target)) return;
2005
+ }
2006
+ this.closeMentionPicker();
2007
+ };
2008
+ document.addEventListener("keydown", onKey, true);
2009
+ document.addEventListener("mousedown", onOutside, true);
2010
+ this.mentionPickerDismiss = () => {
2011
+ document.removeEventListener("keydown", onKey, true);
2012
+ document.removeEventListener("mousedown", onOutside, true);
2013
+ };
2014
+ }
2015
+ closeMentionPicker() {
2016
+ if (this.mentionPickerDismiss) {
2017
+ this.mentionPickerDismiss();
2018
+ this.mentionPickerDismiss = null;
2019
+ }
2020
+ if (this.mentionPicker) {
2021
+ this.mentionPicker.remove();
2022
+ this.mentionPicker = null;
2023
+ }
2024
+ this.mentionPickerFiltered = [];
2025
+ this.mentionPickerSelected = 0;
2026
+ this.mentionPickerEditorRange = null;
2027
+ }
2028
+ /** Position the picker below the given anchor (the @ mention button). */
2029
+ positionMentionPickerByElement(picker, anchor) {
2030
+ const popRect = this.el.getBoundingClientRect();
2031
+ const btnRect = anchor.getBoundingClientRect();
2032
+ picker.style.left = `${btnRect.left - popRect.left}px`;
2033
+ picker.style.top = `${btnRect.bottom - popRect.top + 4}px`;
2034
+ }
2035
+ /** Position the picker below the `@<query>` text range in the editor. */
2036
+ positionMentionPickerByRange(picker, range) {
2037
+ const popRect = this.el.getBoundingClientRect();
2038
+ const rect = range.getBoundingClientRect();
2039
+ picker.style.left = `${rect.left - popRect.left}px`;
2040
+ picker.style.top = `${rect.bottom - popRect.top + 4}px`;
2041
+ }
1705
2042
  /**
1706
2043
  * Insert a mention chip for the given user into the editor at the
1707
- * current caret position. Owner-only ACL in v1: the only callable
1708
- * "mentionable" user is the current Vizu user (= workspace owner).
1709
- * Future versions can expand this with an autocomplete dropdown.
2044
+ * current caret position. Accepts any user-like shape with a required
2045
+ * `id` + `name` — covers both the current VizuUser (for self-mention)
2046
+ * and MentionableUser (from the workspace member picker).
1710
2047
  */
1711
2048
  insertMentionChip(user) {
1712
2049
  if (!user || !user.id) return;
@@ -1783,6 +2120,7 @@ var Popover = class {
1783
2120
  this.el.style.left = left + "px";
1784
2121
  }
1785
2122
  destroy() {
2123
+ this.closeMentionPicker();
1786
2124
  this.el.removeEventListener("click", this.handleClick);
1787
2125
  this.el.remove();
1788
2126
  }
@@ -2561,6 +2899,91 @@ var STYLES = `
2561
2899
  }
2562
2900
  .vz-btn-primary:hover { background: color-mix(in srgb, var(--vz-accent) 88%, white); }
2563
2901
 
2902
+ /* Mention picker \u2014 floating dropdown anchored under the @ mention button */
2903
+ .vz-mention-picker {
2904
+ position: absolute;
2905
+ z-index: 10;
2906
+ min-width: 200px;
2907
+ max-width: 280px;
2908
+ max-height: 240px;
2909
+ overflow-y: auto;
2910
+ padding: 4px;
2911
+ background: var(--vz-bg-popover, #1c1c1c);
2912
+ border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
2913
+ border-radius: 8px;
2914
+ box-shadow: 0 12px 28px rgba(0,0,0,0.4), 0 2px 6px rgba(0,0,0,0.3);
2915
+ display: flex;
2916
+ flex-direction: column;
2917
+ gap: 1px;
2918
+ }
2919
+ .vz-mention-search {
2920
+ width: 100%;
2921
+ padding: 7px 9px;
2922
+ margin-bottom: 4px;
2923
+ background: rgba(255,255,255,0.04);
2924
+ border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
2925
+ border-radius: 6px;
2926
+ color: #fafafa;
2927
+ font-size: 13px;
2928
+ outline: none;
2929
+ box-sizing: border-box;
2930
+ }
2931
+ .vz-mention-search:focus { border-color: var(--vz-accent, #ff8b6e); }
2932
+ .vz-mention-search::placeholder { color: var(--vz-text-muted); }
2933
+ .vz-mention-list {
2934
+ display: flex;
2935
+ flex-direction: column;
2936
+ gap: 1px;
2937
+ min-height: 0;
2938
+ }
2939
+ .vz-mention-loading,
2940
+ .vz-mention-empty {
2941
+ padding: 10px 12px;
2942
+ font-size: 12px;
2943
+ color: var(--vz-text-muted);
2944
+ text-align: center;
2945
+ }
2946
+ .vz-mention-item {
2947
+ display: flex;
2948
+ align-items: center;
2949
+ gap: 8px;
2950
+ padding: 7px 10px;
2951
+ border-radius: 6px;
2952
+ background: transparent;
2953
+ border: none;
2954
+ color: #fafafa;
2955
+ font-size: 13px;
2956
+ text-align: left;
2957
+ cursor: pointer;
2958
+ transition: background 80ms;
2959
+ }
2960
+ .vz-mention-item.is-selected,
2961
+ .vz-mention-item:focus-visible {
2962
+ background: var(--vz-bg-hover, rgba(255,255,255,0.08));
2963
+ outline: none;
2964
+ }
2965
+ .vz-mention-avatar {
2966
+ display: inline-flex;
2967
+ align-items: center;
2968
+ justify-content: center;
2969
+ width: 22px; height: 22px;
2970
+ border-radius: 50%;
2971
+ font-size: 10px; font-weight: 600;
2972
+ color: var(--vz-marker-fg, #0a0a0a);
2973
+ background: var(--vz-accent, #ff8b6e);
2974
+ flex-shrink: 0;
2975
+ overflow: hidden;
2976
+ }
2977
+ .vz-mention-avatar img {
2978
+ width: 100%; height: 100%; object-fit: cover;
2979
+ }
2980
+ .vz-mention-item-name {
2981
+ flex: 1;
2982
+ white-space: nowrap;
2983
+ overflow: hidden;
2984
+ text-overflow: ellipsis;
2985
+ }
2986
+
2564
2987
  /* Sidebar */
2565
2988
  .vz-sidebar {
2566
2989
  position: fixed;
@@ -3328,6 +3751,18 @@ var Vizu = class {
3328
3751
  getUser() {
3329
3752
  return this.user;
3330
3753
  }
3754
+ /**
3755
+ * Fetch the list of users who can be @-mentioned on the current
3756
+ * workspace. Used by the popover's mention dropdown; hosts can also
3757
+ * call this directly to build their own picker. Returns [] for
3758
+ * non-cloud adapters (no workspace, no member list to fetch from).
3759
+ */
3760
+ async searchMentionable() {
3761
+ if (this.storage instanceof CloudStorageAdapter) {
3762
+ return this.storage.searchMentionable();
3763
+ }
3764
+ return [];
3765
+ }
3331
3766
  /**
3332
3767
  * Whether the full Vizu UI (pill, highlighter, markers, popover) can
3333
3768
  * be mounted right now. Cloud-mode workspaces hold off until the
@@ -3683,7 +4118,8 @@ var Vizu = class {
3683
4118
  onClose: () => this.closePopover(),
3684
4119
  onStartReferencePick: (onPicked, onCancel) => this.startPicking(onPicked, onCancel),
3685
4120
  onJumpToReference: (commentId, refId2) => this.jumpToReference(commentId, refId2),
3686
- onAnchorsChanged: (fps) => this.syncActiveAnchorOutlines(fps)
4121
+ onAnchorsChanged: (fps) => this.syncActiveAnchorOutlines(fps),
4122
+ onMentionSearch: () => this.searchMentionable()
3687
4123
  });
3688
4124
  this.popover.setUser(this.user);
3689
4125
  this.sidebar = new Sidebar(this.root, {