@unhingged/vizu-core 0.1.11 → 0.1.13
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 +197 -17
- package/dist/auto.cjs.map +1 -1
- package/dist/auto.js +197 -17
- package/dist/auto.js.map +1 -1
- package/dist/index.cjs +197 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +197 -17
- package/dist/index.js.map +1 -1
- package/dist/vizu.min.js +119 -54
- package/dist/vizu.min.js.map +1 -1
- package/package.json +1 -1
package/dist/auto.js
CHANGED
|
@@ -1235,6 +1235,10 @@ 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;
|
|
1238
1242
|
this.handleEditorKey = (e) => {
|
|
1239
1243
|
const editor = e.currentTarget;
|
|
1240
1244
|
if (e.key === "#") {
|
|
@@ -1341,7 +1345,7 @@ var Popover = class {
|
|
|
1341
1345
|
else if (action === "save") this.save();
|
|
1342
1346
|
else if (action === "mention") {
|
|
1343
1347
|
e.preventDefault();
|
|
1344
|
-
this.
|
|
1348
|
+
this.openMentionPicker(target);
|
|
1345
1349
|
} else if (action === "upload") {
|
|
1346
1350
|
e.preventDefault();
|
|
1347
1351
|
const input = this.el.querySelector('[data-vz="file-input"]');
|
|
@@ -1450,6 +1454,7 @@ var Popover = class {
|
|
|
1450
1454
|
this.position();
|
|
1451
1455
|
}
|
|
1452
1456
|
close() {
|
|
1457
|
+
this.closeMentionPicker();
|
|
1453
1458
|
this.el.style.display = "none";
|
|
1454
1459
|
this.anchorTargets = [];
|
|
1455
1460
|
this.anchorFingerprints = [];
|
|
@@ -1507,7 +1512,7 @@ var Popover = class {
|
|
|
1507
1512
|
<div class="vz-popover-actions">
|
|
1508
1513
|
<button class="vz-btn vz-btn-ghost" data-vz="close">Close</button>
|
|
1509
1514
|
${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
|
|
1515
|
+
${this.currentUser ? `<button class="vz-btn vz-btn-ghost vz-btn-mention" data-vz="mention" title="Mention a teammate">@ mention</button>` : ""}
|
|
1511
1516
|
<button class="vz-btn vz-btn-primary" data-vz="save">Save</button>
|
|
1512
1517
|
</div>
|
|
1513
1518
|
<input type="file" class="vz-file-input" data-vz="file-input" accept="image/png,image/jpeg,image/webp,image/gif,image/svg+xml" hidden />
|
|
@@ -1676,11 +1681,104 @@ var Popover = class {
|
|
|
1676
1681
|
for (const child of editor.childNodes) walk(child);
|
|
1677
1682
|
return parts.join("").trim();
|
|
1678
1683
|
}
|
|
1684
|
+
/**
|
|
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.
|
|
1693
|
+
*/
|
|
1694
|
+
async openMentionPicker(anchor) {
|
|
1695
|
+
this.closeMentionPicker();
|
|
1696
|
+
if (!this.callbacks.onMentionSearch) return;
|
|
1697
|
+
const picker = document.createElement("div");
|
|
1698
|
+
picker.className = "vz-mention-picker";
|
|
1699
|
+
picker.setAttribute("data-vz-ignore", "");
|
|
1700
|
+
picker.innerHTML = `<div class="vz-mention-loading">Loading\u2026</div>`;
|
|
1701
|
+
this.el.appendChild(picker);
|
|
1702
|
+
this.mentionPicker = picker;
|
|
1703
|
+
this.positionMentionPicker(picker, anchor);
|
|
1704
|
+
let users = [];
|
|
1705
|
+
try {
|
|
1706
|
+
users = await this.callbacks.onMentionSearch();
|
|
1707
|
+
} catch {
|
|
1708
|
+
}
|
|
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();
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const itemsHtml = users.map((u) => {
|
|
1717
|
+
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>`;
|
|
1719
|
+
}).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();
|
|
1733
|
+
}
|
|
1734
|
+
/** Wires Esc + outside-click handlers to close the open picker. */
|
|
1735
|
+
wireMentionPickerDismiss() {
|
|
1736
|
+
if (this.mentionPickerDismiss) return;
|
|
1737
|
+
const onKey = (e) => {
|
|
1738
|
+
if (e.key === "Escape") {
|
|
1739
|
+
e.preventDefault();
|
|
1740
|
+
this.closeMentionPicker();
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
const onOutside = (e) => {
|
|
1744
|
+
if (!this.mentionPicker) return;
|
|
1745
|
+
if (this.mentionPicker.contains(e.target)) return;
|
|
1746
|
+
this.closeMentionPicker();
|
|
1747
|
+
};
|
|
1748
|
+
document.addEventListener("keydown", onKey, true);
|
|
1749
|
+
document.addEventListener("mousedown", onOutside, true);
|
|
1750
|
+
this.mentionPickerDismiss = () => {
|
|
1751
|
+
document.removeEventListener("keydown", onKey, true);
|
|
1752
|
+
document.removeEventListener("mousedown", onOutside, true);
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
closeMentionPicker() {
|
|
1756
|
+
if (this.mentionPickerDismiss) {
|
|
1757
|
+
this.mentionPickerDismiss();
|
|
1758
|
+
this.mentionPickerDismiss = null;
|
|
1759
|
+
}
|
|
1760
|
+
if (this.mentionPicker) {
|
|
1761
|
+
this.mentionPicker.remove();
|
|
1762
|
+
this.mentionPicker = null;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
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) {
|
|
1770
|
+
const popRect = this.el.getBoundingClientRect();
|
|
1771
|
+
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`;
|
|
1776
|
+
}
|
|
1679
1777
|
/**
|
|
1680
1778
|
* Insert a mention chip for the given user into the editor at the
|
|
1681
|
-
* current caret position.
|
|
1682
|
-
*
|
|
1683
|
-
*
|
|
1779
|
+
* current caret position. Accepts any user-like shape with a required
|
|
1780
|
+
* `id` + `name` — covers both the current VizuUser (for self-mention)
|
|
1781
|
+
* and MentionableUser (from the workspace member picker).
|
|
1684
1782
|
*/
|
|
1685
1783
|
insertMentionChip(user) {
|
|
1686
1784
|
if (!user || !user.id) return;
|
|
@@ -1757,6 +1855,7 @@ var Popover = class {
|
|
|
1757
1855
|
this.el.style.left = left + "px";
|
|
1758
1856
|
}
|
|
1759
1857
|
destroy() {
|
|
1858
|
+
this.closeMentionPicker();
|
|
1760
1859
|
this.el.removeEventListener("click", this.handleClick);
|
|
1761
1860
|
this.el.remove();
|
|
1762
1861
|
}
|
|
@@ -2535,6 +2634,71 @@ var STYLES = `
|
|
|
2535
2634
|
}
|
|
2536
2635
|
.vz-btn-primary:hover { background: color-mix(in srgb, var(--vz-accent) 88%, white); }
|
|
2537
2636
|
|
|
2637
|
+
/* Mention picker \u2014 floating dropdown anchored under the @ mention button */
|
|
2638
|
+
.vz-mention-picker {
|
|
2639
|
+
position: absolute;
|
|
2640
|
+
z-index: 10;
|
|
2641
|
+
min-width: 200px;
|
|
2642
|
+
max-width: 280px;
|
|
2643
|
+
max-height: 240px;
|
|
2644
|
+
overflow-y: auto;
|
|
2645
|
+
padding: 4px;
|
|
2646
|
+
background: var(--vz-bg-popover, #1c1c1c);
|
|
2647
|
+
border: 1px solid var(--vz-border, rgba(255,255,255,0.08));
|
|
2648
|
+
border-radius: 8px;
|
|
2649
|
+
box-shadow: 0 12px 28px rgba(0,0,0,0.4), 0 2px 6px rgba(0,0,0,0.3);
|
|
2650
|
+
display: flex;
|
|
2651
|
+
flex-direction: column;
|
|
2652
|
+
gap: 1px;
|
|
2653
|
+
}
|
|
2654
|
+
.vz-mention-loading,
|
|
2655
|
+
.vz-mention-empty {
|
|
2656
|
+
padding: 10px 12px;
|
|
2657
|
+
font-size: 12px;
|
|
2658
|
+
color: var(--vz-text-muted);
|
|
2659
|
+
text-align: center;
|
|
2660
|
+
}
|
|
2661
|
+
.vz-mention-item {
|
|
2662
|
+
display: flex;
|
|
2663
|
+
align-items: center;
|
|
2664
|
+
gap: 8px;
|
|
2665
|
+
padding: 7px 10px;
|
|
2666
|
+
border-radius: 6px;
|
|
2667
|
+
background: transparent;
|
|
2668
|
+
border: none;
|
|
2669
|
+
color: #fafafa;
|
|
2670
|
+
font-size: 13px;
|
|
2671
|
+
text-align: left;
|
|
2672
|
+
cursor: pointer;
|
|
2673
|
+
transition: background 80ms;
|
|
2674
|
+
}
|
|
2675
|
+
.vz-mention-item:hover,
|
|
2676
|
+
.vz-mention-item:focus-visible {
|
|
2677
|
+
background: var(--vz-bg-hover, rgba(255,255,255,0.06));
|
|
2678
|
+
outline: none;
|
|
2679
|
+
}
|
|
2680
|
+
.vz-mention-avatar {
|
|
2681
|
+
display: inline-flex;
|
|
2682
|
+
align-items: center;
|
|
2683
|
+
justify-content: center;
|
|
2684
|
+
width: 22px; height: 22px;
|
|
2685
|
+
border-radius: 50%;
|
|
2686
|
+
font-size: 10px; font-weight: 600;
|
|
2687
|
+
color: var(--vz-marker-fg, #0a0a0a);
|
|
2688
|
+
background: var(--vz-accent, #ff8b6e);
|
|
2689
|
+
flex-shrink: 0;
|
|
2690
|
+
overflow: hidden;
|
|
2691
|
+
}
|
|
2692
|
+
.vz-mention-avatar img {
|
|
2693
|
+
width: 100%; height: 100%; object-fit: cover;
|
|
2694
|
+
}
|
|
2695
|
+
.vz-mention-item-name {
|
|
2696
|
+
flex: 1;
|
|
2697
|
+
white-space: nowrap;
|
|
2698
|
+
overflow: hidden;
|
|
2699
|
+
text-overflow: ellipsis;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2538
2702
|
/* Sidebar */
|
|
2539
2703
|
.vz-sidebar {
|
|
2540
2704
|
position: fixed;
|
|
@@ -3294,22 +3458,35 @@ var Vizu = class {
|
|
|
3294
3458
|
this.user = user;
|
|
3295
3459
|
this.popover?.setUser(user);
|
|
3296
3460
|
this.pill?.setUser(user);
|
|
3297
|
-
if (this.pill) {
|
|
3298
|
-
if (this.shouldShowPill()) this.pill.show();
|
|
3299
|
-
else this.pill.hide();
|
|
3300
|
-
}
|
|
3301
3461
|
this.bus.emit("user:changed", { user });
|
|
3462
|
+
if (this.enabled && !this.root && this.shouldMount()) {
|
|
3463
|
+
this.deferred(() => this.mount());
|
|
3464
|
+
}
|
|
3302
3465
|
}
|
|
3303
3466
|
getUser() {
|
|
3304
3467
|
return this.user;
|
|
3305
3468
|
}
|
|
3306
3469
|
/**
|
|
3307
|
-
*
|
|
3308
|
-
*
|
|
3309
|
-
*
|
|
3310
|
-
*
|
|
3470
|
+
* Fetch the list of users who can be @-mentioned on the current
|
|
3471
|
+
* workspace. Used by the popover's mention dropdown; hosts can also
|
|
3472
|
+
* call this directly to build their own picker. Returns [] for
|
|
3473
|
+
* non-cloud adapters (no workspace, no member list to fetch from).
|
|
3311
3474
|
*/
|
|
3312
|
-
|
|
3475
|
+
async searchMentionable() {
|
|
3476
|
+
if (this.storage instanceof CloudStorageAdapter) {
|
|
3477
|
+
return this.storage.searchMentionable();
|
|
3478
|
+
}
|
|
3479
|
+
return [];
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* Whether the full Vizu UI (pill, highlighter, markers, popover) can
|
|
3483
|
+
* be mounted right now. Cloud-mode workspaces hold off until the
|
|
3484
|
+
* user is signed in; non-cloud usage (local / memory storage) has no
|
|
3485
|
+
* auth concept and mounts immediately. Re-checked from `enable()`
|
|
3486
|
+
* and from `setUser()` so that the moment auth resolves, the UI
|
|
3487
|
+
* appears without the user needing to press the shortcut again.
|
|
3488
|
+
*/
|
|
3489
|
+
shouldMount() {
|
|
3313
3490
|
if (!(this.storage instanceof CloudStorageAdapter)) return true;
|
|
3314
3491
|
return this.user !== null;
|
|
3315
3492
|
}
|
|
@@ -3336,7 +3513,9 @@ var Vizu = class {
|
|
|
3336
3513
|
if (this.enabled) return;
|
|
3337
3514
|
this.enabled = true;
|
|
3338
3515
|
this.bus.emit("enabled", {});
|
|
3339
|
-
|
|
3516
|
+
if (this.shouldMount()) {
|
|
3517
|
+
this.deferred(() => this.mount());
|
|
3518
|
+
}
|
|
3340
3519
|
}
|
|
3341
3520
|
disable() {
|
|
3342
3521
|
if (!this.enabled) return;
|
|
@@ -3654,7 +3833,8 @@ var Vizu = class {
|
|
|
3654
3833
|
onClose: () => this.closePopover(),
|
|
3655
3834
|
onStartReferencePick: (onPicked, onCancel) => this.startPicking(onPicked, onCancel),
|
|
3656
3835
|
onJumpToReference: (commentId, refId2) => this.jumpToReference(commentId, refId2),
|
|
3657
|
-
onAnchorsChanged: (fps) => this.syncActiveAnchorOutlines(fps)
|
|
3836
|
+
onAnchorsChanged: (fps) => this.syncActiveAnchorOutlines(fps),
|
|
3837
|
+
onMentionSearch: () => this.searchMentionable()
|
|
3658
3838
|
});
|
|
3659
3839
|
this.popover.setUser(this.user);
|
|
3660
3840
|
this.sidebar = new Sidebar(this.root, {
|
|
@@ -3676,7 +3856,7 @@ var Vizu = class {
|
|
|
3676
3856
|
this.pill.setUser(this.user);
|
|
3677
3857
|
this.pill.setComments(this.comments);
|
|
3678
3858
|
this.pill.setMultiSelectState(this.multiSelectMode, this.pendingFingerprints.length);
|
|
3679
|
-
|
|
3859
|
+
this.pill.show();
|
|
3680
3860
|
this.highlighter = new Highlighter(
|
|
3681
3861
|
this.root,
|
|
3682
3862
|
this.opts.ignoreSelectors ?? [],
|