@unhingged/vizu-core 0.1.10 → 0.1.12

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.d.cts CHANGED
@@ -48,6 +48,18 @@ interface VizuUser {
48
48
  /** Free-form host-defined extras (team, role, etc.) preserved on each comment. */
49
49
  meta?: Record<string, unknown>;
50
50
  }
51
+ /**
52
+ * Slim shape for the mention picker. The host can @-mention any of
53
+ * these in a comment body. Returned by `Vizu.searchMentionable()` and
54
+ * cached by the popover dropdown. Name + avatar is all the UI needs;
55
+ * email/role intentionally omitted to keep the cross-origin surface
56
+ * minimal.
57
+ */
58
+ interface MentionableUser {
59
+ id: string;
60
+ name: string;
61
+ avatarUrl?: string;
62
+ }
51
63
  /** Current schema version. Bump + add a migration when the shape changes. */
52
64
  declare const SCHEMA_VERSION: 2;
53
65
  type SchemaVersion = typeof SCHEMA_VERSION;
@@ -501,6 +513,13 @@ declare class CloudStorageAdapter implements StorageAdapter {
501
513
  isAccessDenied(): boolean;
502
514
  /** Last denial reason; null when not denied. */
503
515
  accessDenialReason(): string | null;
516
+ /**
517
+ * Fetch the list of users who can be @-mentioned on this workspace.
518
+ * Used by the popover's mention dropdown — returns owner + joined
519
+ * editors/viewers, sorted owner-first. Silent on no auth (returns [])
520
+ * so popover doesn't open a popup just to render an empty dropdown.
521
+ */
522
+ searchMentionable(): Promise<MentionableUser[]>;
504
523
  load(_namespace: string): Promise<VizuComment[]>;
505
524
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
506
525
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -659,12 +678,14 @@ declare class Vizu {
659
678
  setUser(user: VizuUser | null): void;
660
679
  getUser(): VizuUser | null;
661
680
  /**
662
- * Whether the pill belongs on-screen right now. Cloud-mode workspaces
663
- * keep it hidden until the user is signed in; non-cloud usage (local /
664
- * memory storage) has no auth and always shows it. Recomputed on every
665
- * setUser and on mount.
681
+ * Whether the full Vizu UI (pill, highlighter, markers, popover) can
682
+ * be mounted right now. Cloud-mode workspaces hold off until the
683
+ * user is signed in; non-cloud usage (local / memory storage) has no
684
+ * auth concept and mounts immediately. Re-checked from `enable()`
685
+ * and from `setUser()` so that the moment auth resolves, the UI
686
+ * appears without the user needing to press the shortcut again.
666
687
  */
667
- private shouldShowPill;
688
+ private shouldMount;
668
689
  /**
669
690
  * Cloud-mode write gate. Every entry point that creates or modifies a
670
691
  * comment first checks that we have a known user. Without one, the
package/dist/index.d.ts CHANGED
@@ -48,6 +48,18 @@ interface VizuUser {
48
48
  /** Free-form host-defined extras (team, role, etc.) preserved on each comment. */
49
49
  meta?: Record<string, unknown>;
50
50
  }
51
+ /**
52
+ * Slim shape for the mention picker. The host can @-mention any of
53
+ * these in a comment body. Returned by `Vizu.searchMentionable()` and
54
+ * cached by the popover dropdown. Name + avatar is all the UI needs;
55
+ * email/role intentionally omitted to keep the cross-origin surface
56
+ * minimal.
57
+ */
58
+ interface MentionableUser {
59
+ id: string;
60
+ name: string;
61
+ avatarUrl?: string;
62
+ }
51
63
  /** Current schema version. Bump + add a migration when the shape changes. */
52
64
  declare const SCHEMA_VERSION: 2;
53
65
  type SchemaVersion = typeof SCHEMA_VERSION;
@@ -501,6 +513,13 @@ declare class CloudStorageAdapter implements StorageAdapter {
501
513
  isAccessDenied(): boolean;
502
514
  /** Last denial reason; null when not denied. */
503
515
  accessDenialReason(): string | null;
516
+ /**
517
+ * Fetch the list of users who can be @-mentioned on this workspace.
518
+ * Used by the popover's mention dropdown — returns owner + joined
519
+ * editors/viewers, sorted owner-first. Silent on no auth (returns [])
520
+ * so popover doesn't open a popup just to render an empty dropdown.
521
+ */
522
+ searchMentionable(): Promise<MentionableUser[]>;
504
523
  load(_namespace: string): Promise<VizuComment[]>;
505
524
  addComment(_namespace: string, comment: VizuComment): Promise<void>;
506
525
  updateComment(_namespace: string, id: string, patch: Partial<VizuComment>): Promise<VizuComment | null>;
@@ -659,12 +678,14 @@ declare class Vizu {
659
678
  setUser(user: VizuUser | null): void;
660
679
  getUser(): VizuUser | null;
661
680
  /**
662
- * Whether the pill belongs on-screen right now. Cloud-mode workspaces
663
- * keep it hidden until the user is signed in; non-cloud usage (local /
664
- * memory storage) has no auth and always shows it. Recomputed on every
665
- * setUser and on mount.
681
+ * Whether the full Vizu UI (pill, highlighter, markers, popover) can
682
+ * be mounted right now. Cloud-mode workspaces hold off until the
683
+ * user is signed in; non-cloud usage (local / memory storage) has no
684
+ * auth concept and mounts immediately. Re-checked from `enable()`
685
+ * and from `setUser()` so that the moment auth resolves, the UI
686
+ * appears without the user needing to press the shortcut again.
666
687
  */
667
- private shouldShowPill;
688
+ private shouldMount;
668
689
  /**
669
690
  * Cloud-mode write gate. Every entry point that creates or modifies a
670
691
  * comment first checks that we have a known user. Without one, the
package/dist/index.js CHANGED
@@ -12,7 +12,8 @@ function isInside(target, selector) {
12
12
  return !!target.closest(selector);
13
13
  }
14
14
  function escapeHtml(s) {
15
- return s.replace(/[&<>"']/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch]);
15
+ if (s == null) return "";
16
+ return String(s).replace(/[&<>"']/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch]);
16
17
  }
17
18
  function formatTime(ts) {
18
19
  const d = new Date(ts);
@@ -24,14 +25,16 @@ function formatTime(ts) {
24
25
  return d.toLocaleDateString() + " " + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
25
26
  }
26
27
  function initials(name) {
27
- return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0].toUpperCase()).join("");
28
+ if (!name) return "";
29
+ return String(name).split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0].toUpperCase()).join("");
28
30
  }
29
31
  function avatarHtml(user, className = "vz-comment-author-avatar") {
30
32
  if (!user) return `<span class="${className}">\xB7</span>`;
33
+ const displayName = user.name || user.id || "User";
31
34
  if (user.avatarUrl) {
32
- return `<span class="${className}"><img src="${escapeHtml(user.avatarUrl)}" alt="${escapeHtml(user.name)}" /></span>`;
35
+ return `<span class="${className}"><img src="${escapeHtml(user.avatarUrl)}" alt="${escapeHtml(displayName)}" /></span>`;
33
36
  }
34
- return `<span class="${className}">${escapeHtml(initials(user.name) || "?")}</span>`;
37
+ return `<span class="${className}">${escapeHtml(initials(displayName) || "?")}</span>`;
35
38
  }
36
39
  function refId() {
37
40
  return Math.random().toString(36).slice(2, 10);
@@ -434,6 +437,26 @@ var CloudStorageAdapter = class {
434
437
  accessDenialReason() {
435
438
  return this.accessDeniedReason;
436
439
  }
440
+ /**
441
+ * Fetch the list of users who can be @-mentioned on this workspace.
442
+ * Used by the popover's mention dropdown — returns owner + joined
443
+ * editors/viewers, sorted owner-first. Silent on no auth (returns [])
444
+ * so popover doesn't open a popup just to render an empty dropdown.
445
+ */
446
+ async searchMentionable() {
447
+ if (!this.cachedToken || this.isExpired(this.cachedToken)) return [];
448
+ try {
449
+ const res = await this.fetchAuthed(
450
+ this.apiUrl + "/api/workspaces/" + encodeURIComponent(this.workspace) + "/mentionable",
451
+ { method: "GET" }
452
+ );
453
+ if (!res.ok) return [];
454
+ const json = await this.parseJson(res);
455
+ return Array.isArray(json?.users) ? json.users : [];
456
+ } catch {
457
+ return [];
458
+ }
459
+ }
437
460
  /* ─── StorageAdapter v2 ───────────────────────────────────────────── */
438
461
  async load(_namespace) {
439
462
  if (!this.cachedToken || this.isExpired(this.cachedToken)) {
@@ -1183,7 +1206,7 @@ function renderRepliesHtml(c) {
1183
1206
  const items = replies.map(
1184
1207
  (r) => `
1185
1208
  <div class="vz-reply" data-reply-id="${escapeHtml(r.id)}">
1186
- ${r.author ? `<div class="vz-comment-author">${avatarHtml(r.author)} <span class="vz-comment-author-name">${escapeHtml(r.author.name)}</span></div>` : ""}
1209
+ ${r.author ? `<div class="vz-comment-author">${avatarHtml(r.author)} <span class="vz-comment-author-name">${escapeHtml(r.author.name || r.author.id || "User")}</span></div>` : ""}
1187
1210
  <div class="vz-reply-text">${escapeHtml(r.text)}</div>
1188
1211
  <div class="vz-comment-meta">
1189
1212
  <span>${escapeHtml(formatTime(typeof r.createdAt === "number" ? r.createdAt : new Date(r.createdAt).getTime()))}</span>
@@ -1460,7 +1483,7 @@ var Popover = class {
1460
1483
  ${comments.length === 0 ? '<div class="vz-empty">No comments yet.</div>' : comments.map(
1461
1484
  (c) => `
1462
1485
  <div class="vz-comment" data-comment-id="${escapeHtml(c.id)}">
1463
- ${c.author ? `<div class="vz-comment-author">${avatarHtml(c.author)} <span class="vz-comment-author-name">${escapeHtml(c.author.name)}</span></div>` : ""}
1486
+ ${c.author ? `<div class="vz-comment-author">${avatarHtml(c.author)} <span class="vz-comment-author-name">${escapeHtml(c.author.name || c.author.id || "User")}</span></div>` : ""}
1464
1487
  <div>${renderCommentText(c.text, c.id)}</div>
1465
1488
  ${renderAttachmentsHtml(c.attachments)}
1466
1489
  <div class="vz-comment-meta">
@@ -3271,22 +3294,23 @@ var Vizu = class {
3271
3294
  this.user = user;
3272
3295
  this.popover?.setUser(user);
3273
3296
  this.pill?.setUser(user);
3274
- if (this.pill) {
3275
- if (this.shouldShowPill()) this.pill.show();
3276
- else this.pill.hide();
3277
- }
3278
3297
  this.bus.emit("user:changed", { user });
3298
+ if (this.enabled && !this.root && this.shouldMount()) {
3299
+ this.deferred(() => this.mount());
3300
+ }
3279
3301
  }
3280
3302
  getUser() {
3281
3303
  return this.user;
3282
3304
  }
3283
3305
  /**
3284
- * Whether the pill belongs on-screen right now. Cloud-mode workspaces
3285
- * keep it hidden until the user is signed in; non-cloud usage (local /
3286
- * memory storage) has no auth and always shows it. Recomputed on every
3287
- * setUser and on mount.
3306
+ * Whether the full Vizu UI (pill, highlighter, markers, popover) can
3307
+ * be mounted right now. Cloud-mode workspaces hold off until the
3308
+ * user is signed in; non-cloud usage (local / memory storage) has no
3309
+ * auth concept and mounts immediately. Re-checked from `enable()`
3310
+ * and from `setUser()` so that the moment auth resolves, the UI
3311
+ * appears without the user needing to press the shortcut again.
3288
3312
  */
3289
- shouldShowPill() {
3313
+ shouldMount() {
3290
3314
  if (!(this.storage instanceof CloudStorageAdapter)) return true;
3291
3315
  return this.user !== null;
3292
3316
  }
@@ -3313,7 +3337,9 @@ var Vizu = class {
3313
3337
  if (this.enabled) return;
3314
3338
  this.enabled = true;
3315
3339
  this.bus.emit("enabled", {});
3316
- this.deferred(() => this.mount());
3340
+ if (this.shouldMount()) {
3341
+ this.deferred(() => this.mount());
3342
+ }
3317
3343
  }
3318
3344
  disable() {
3319
3345
  if (!this.enabled) return;
@@ -3653,7 +3679,7 @@ var Vizu = class {
3653
3679
  this.pill.setUser(this.user);
3654
3680
  this.pill.setComments(this.comments);
3655
3681
  this.pill.setMultiSelectState(this.multiSelectMode, this.pendingFingerprints.length);
3656
- if (this.shouldShowPill()) this.pill.show();
3682
+ this.pill.show();
3657
3683
  this.highlighter = new Highlighter(
3658
3684
  this.root,
3659
3685
  this.opts.ignoreSelectors ?? [],