@unhingged/vizu-core 0.1.2 → 0.1.4
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 +682 -1848
- package/dist/auto.cjs.map +1 -1
- package/dist/auto.js +310 -57
- package/dist/auto.js.map +1 -1
- package/dist/index.cjs +681 -1847
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -39
- package/dist/index.d.ts +11 -39
- package/dist/index.js +310 -57
- package/dist/index.js.map +1 -1
- package/dist/vizu.min.js +55 -234
- package/dist/vizu.min.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-OMIFOSQ2.js +0 -295
- package/dist/chunk-OMIFOSQ2.js.map +0 -1
- package/dist/live-collab-BVLNJ5QI.js +0 -1098
- package/dist/live-collab-BVLNJ5QI.js.map +0 -1
|
@@ -1,1098 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
findByFingerprint
|
|
3
|
-
} from "./chunk-OMIFOSQ2.js";
|
|
4
|
-
|
|
5
|
-
// src/cursor-colors.ts
|
|
6
|
-
var CURSOR_COLORS = [
|
|
7
|
-
"#4F46E5",
|
|
8
|
-
// indigo
|
|
9
|
-
"#15803D",
|
|
10
|
-
// forest
|
|
11
|
-
"#C2410C",
|
|
12
|
-
// orange
|
|
13
|
-
"#BE185D",
|
|
14
|
-
// rose
|
|
15
|
-
"#0E7490",
|
|
16
|
-
// cyan
|
|
17
|
-
"#7E22CE",
|
|
18
|
-
// plum
|
|
19
|
-
"#DC2626",
|
|
20
|
-
// red
|
|
21
|
-
"#0F766E"
|
|
22
|
-
// teal
|
|
23
|
-
];
|
|
24
|
-
function colorForUser(userId) {
|
|
25
|
-
let h = 0;
|
|
26
|
-
for (let i = 0; i < userId.length; i++) {
|
|
27
|
-
h = h * 31 + userId.charCodeAt(i) | 0;
|
|
28
|
-
}
|
|
29
|
-
return CURSOR_COLORS[Math.abs(h) % CURSOR_COLORS.length];
|
|
30
|
-
}
|
|
31
|
-
function prefersReducedMotion() {
|
|
32
|
-
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
33
|
-
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// src/presence-stack.ts
|
|
37
|
-
var Z_INDEX = 2147483640;
|
|
38
|
-
var MAX_VISIBLE = 5;
|
|
39
|
-
var AVATAR_SIZE = 28;
|
|
40
|
-
var OVERLAP_PX = 6;
|
|
41
|
-
var PresenceStack = class {
|
|
42
|
-
constructor(opts) {
|
|
43
|
-
this.container = null;
|
|
44
|
-
this.avatars = /* @__PURE__ */ new Map();
|
|
45
|
-
this.overflowEl = null;
|
|
46
|
-
this.followingUserId = null;
|
|
47
|
-
this.onAvatarClick = null;
|
|
48
|
-
this.onAvatarClick = opts?.onAvatarClick ?? null;
|
|
49
|
-
}
|
|
50
|
-
mount() {
|
|
51
|
-
if (typeof document === "undefined") return;
|
|
52
|
-
if (this.container) return;
|
|
53
|
-
this.container = document.createElement("div");
|
|
54
|
-
this.container.setAttribute("data-vizu-presence-stack", "");
|
|
55
|
-
this.container.style.cssText = `
|
|
56
|
-
position: fixed;
|
|
57
|
-
top: 16px;
|
|
58
|
-
right: 16px;
|
|
59
|
-
z-index: ${Z_INDEX};
|
|
60
|
-
display: flex;
|
|
61
|
-
align-items: center;
|
|
62
|
-
pointer-events: auto;
|
|
63
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
64
|
-
`;
|
|
65
|
-
document.body.appendChild(this.container);
|
|
66
|
-
}
|
|
67
|
-
destroy() {
|
|
68
|
-
for (const el of this.avatars.values()) el.remove();
|
|
69
|
-
this.avatars.clear();
|
|
70
|
-
this.overflowEl?.remove();
|
|
71
|
-
this.overflowEl = null;
|
|
72
|
-
this.container?.remove();
|
|
73
|
-
this.container = null;
|
|
74
|
-
}
|
|
75
|
-
setOthers(others) {
|
|
76
|
-
if (!this.container) return;
|
|
77
|
-
const visible = others.slice(0, MAX_VISIBLE);
|
|
78
|
-
const overflow = Math.max(0, others.length - visible.length);
|
|
79
|
-
const nextIds = new Set(visible.map((u) => u.id));
|
|
80
|
-
for (const [id, el] of Array.from(this.avatars)) {
|
|
81
|
-
if (!nextIds.has(id)) {
|
|
82
|
-
el.remove();
|
|
83
|
-
this.avatars.delete(id);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
for (const user of visible) {
|
|
87
|
-
let el = this.avatars.get(user.id);
|
|
88
|
-
if (!el) {
|
|
89
|
-
el = createAvatar(user, () => this.onAvatarClick?.(user.id));
|
|
90
|
-
this.avatars.set(user.id, el);
|
|
91
|
-
} else {
|
|
92
|
-
updateAvatar(el, user);
|
|
93
|
-
}
|
|
94
|
-
paintAvatarFollowState(el, user, this.followingUserId === user.id);
|
|
95
|
-
this.container.appendChild(el);
|
|
96
|
-
}
|
|
97
|
-
this.renderOverflow(overflow);
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Mark a user as the active followee so its avatar can render the
|
|
101
|
-
* coral ring highlight. Pass null to clear.
|
|
102
|
-
*/
|
|
103
|
-
setFollowing(userId) {
|
|
104
|
-
this.followingUserId = userId;
|
|
105
|
-
for (const [id, el] of this.avatars) {
|
|
106
|
-
const user = avatarMeta(el);
|
|
107
|
-
if (user) paintAvatarFollowState(el, user, id === userId);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/* ────────────────── private ────────────────── */
|
|
111
|
-
renderOverflow(count) {
|
|
112
|
-
if (!this.container) return;
|
|
113
|
-
if (count <= 0) {
|
|
114
|
-
this.overflowEl?.remove();
|
|
115
|
-
this.overflowEl = null;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (!this.overflowEl) {
|
|
119
|
-
this.overflowEl = createOverflowChip();
|
|
120
|
-
}
|
|
121
|
-
this.overflowEl.textContent = `+${count}`;
|
|
122
|
-
this.container.appendChild(this.overflowEl);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
function createAvatar(user, onClick) {
|
|
126
|
-
const wrap = document.createElement("div");
|
|
127
|
-
wrap.setAttribute("data-vizu-presence-avatar", user.id);
|
|
128
|
-
wrap.setAttribute("data-vizu-presence-name", user.name);
|
|
129
|
-
wrap.setAttribute("data-vizu-presence-color", user.color);
|
|
130
|
-
if (user.avatar) wrap.setAttribute("data-vizu-presence-img", user.avatar);
|
|
131
|
-
const reduced = prefersReducedMotion();
|
|
132
|
-
const hoverTransition = reduced ? "none" : "transform 130ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 130ms ease-out";
|
|
133
|
-
wrap.style.cssText = `
|
|
134
|
-
position: relative;
|
|
135
|
-
width: ${AVATAR_SIZE}px;
|
|
136
|
-
height: ${AVATAR_SIZE}px;
|
|
137
|
-
margin-left: -${OVERLAP_PX}px;
|
|
138
|
-
border-radius: 50%;
|
|
139
|
-
background: ${user.color};
|
|
140
|
-
padding: 1.5px;
|
|
141
|
-
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.95), 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
142
|
-
cursor: pointer;
|
|
143
|
-
flex-shrink: 0;
|
|
144
|
-
transition: ${hoverTransition};
|
|
145
|
-
`;
|
|
146
|
-
wrap.addEventListener("click", (e) => {
|
|
147
|
-
e.preventDefault();
|
|
148
|
-
e.stopPropagation();
|
|
149
|
-
onClick();
|
|
150
|
-
});
|
|
151
|
-
if (!reduced) {
|
|
152
|
-
wrap.addEventListener(
|
|
153
|
-
"mouseenter",
|
|
154
|
-
() => wrap.style.transform = "translateY(-1px) scale(1.06)"
|
|
155
|
-
);
|
|
156
|
-
wrap.addEventListener("mouseleave", () => wrap.style.transform = "");
|
|
157
|
-
}
|
|
158
|
-
const inner = document.createElement("div");
|
|
159
|
-
inner.setAttribute("data-vizu-presence-inner", "");
|
|
160
|
-
inner.style.cssText = `
|
|
161
|
-
width: 100%;
|
|
162
|
-
height: 100%;
|
|
163
|
-
border-radius: 50%;
|
|
164
|
-
overflow: hidden;
|
|
165
|
-
background: ${user.color};
|
|
166
|
-
color: #ffffff;
|
|
167
|
-
display: flex;
|
|
168
|
-
align-items: center;
|
|
169
|
-
justify-content: center;
|
|
170
|
-
font-size: 10.5px;
|
|
171
|
-
font-weight: 600;
|
|
172
|
-
letter-spacing: -0.01em;
|
|
173
|
-
line-height: 1;
|
|
174
|
-
user-select: none;
|
|
175
|
-
`;
|
|
176
|
-
paintAvatarContent(inner, user);
|
|
177
|
-
wrap.appendChild(inner);
|
|
178
|
-
const tip = createTooltip(user.name);
|
|
179
|
-
wrap.appendChild(tip);
|
|
180
|
-
wrap.addEventListener("mouseenter", () => {
|
|
181
|
-
tip.style.opacity = "1";
|
|
182
|
-
tip.style.transform = "translateY(0)";
|
|
183
|
-
});
|
|
184
|
-
wrap.addEventListener("mouseleave", () => {
|
|
185
|
-
tip.style.opacity = "0";
|
|
186
|
-
tip.style.transform = "translateY(-2px)";
|
|
187
|
-
});
|
|
188
|
-
return wrap;
|
|
189
|
-
}
|
|
190
|
-
function updateAvatar(wrap, user) {
|
|
191
|
-
const inner = wrap.querySelector("[data-vizu-presence-inner]");
|
|
192
|
-
if (inner) paintAvatarContent(inner, user);
|
|
193
|
-
const tip = wrap.querySelector("[data-vizu-presence-tip]");
|
|
194
|
-
if (tip) tip.textContent = user.name;
|
|
195
|
-
wrap.setAttribute("data-vizu-presence-name", user.name);
|
|
196
|
-
if (user.avatar) wrap.setAttribute("data-vizu-presence-img", user.avatar);
|
|
197
|
-
else wrap.removeAttribute("data-vizu-presence-img");
|
|
198
|
-
}
|
|
199
|
-
function avatarMeta(wrap) {
|
|
200
|
-
const id = wrap.getAttribute("data-vizu-presence-avatar");
|
|
201
|
-
const name = wrap.getAttribute("data-vizu-presence-name");
|
|
202
|
-
const color = wrap.getAttribute("data-vizu-presence-color");
|
|
203
|
-
if (!id || !name || !color) return null;
|
|
204
|
-
const avatar = wrap.getAttribute("data-vizu-presence-img") ?? void 0;
|
|
205
|
-
return { id, name, color, avatar };
|
|
206
|
-
}
|
|
207
|
-
function paintAvatarFollowState(wrap, user, isFollowing) {
|
|
208
|
-
if (isFollowing) {
|
|
209
|
-
wrap.style.boxShadow = `0 0 0 2px rgba(255, 255, 255, 0.95), 0 0 0 4px var(--vizu-coral, #FF6647), 0 4px 12px rgba(255, 102, 71, 0.4)`;
|
|
210
|
-
} else {
|
|
211
|
-
wrap.style.boxShadow = `0 0 0 2px rgba(255, 255, 255, 0.95), 0 2px 6px rgba(0, 0, 0, 0.15)`;
|
|
212
|
-
}
|
|
213
|
-
void user;
|
|
214
|
-
}
|
|
215
|
-
function paintAvatarContent(inner, user) {
|
|
216
|
-
inner.textContent = "";
|
|
217
|
-
if (user.avatar) {
|
|
218
|
-
const img = document.createElement("img");
|
|
219
|
-
img.src = user.avatar;
|
|
220
|
-
img.alt = user.name;
|
|
221
|
-
img.referrerPolicy = "no-referrer";
|
|
222
|
-
img.style.cssText = "width: 100%; height: 100%; object-fit: cover; display: block;";
|
|
223
|
-
img.addEventListener(
|
|
224
|
-
"error",
|
|
225
|
-
() => {
|
|
226
|
-
img.remove();
|
|
227
|
-
inner.textContent = initialsOf(user.name);
|
|
228
|
-
},
|
|
229
|
-
{ once: true }
|
|
230
|
-
);
|
|
231
|
-
inner.appendChild(img);
|
|
232
|
-
} else {
|
|
233
|
-
inner.textContent = initialsOf(user.name);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function createTooltip(text) {
|
|
237
|
-
const tip = document.createElement("div");
|
|
238
|
-
tip.setAttribute("data-vizu-presence-tip", "");
|
|
239
|
-
tip.textContent = text;
|
|
240
|
-
tip.style.cssText = `
|
|
241
|
-
position: absolute;
|
|
242
|
-
top: calc(100% + 6px);
|
|
243
|
-
right: 0;
|
|
244
|
-
background: rgba(10, 10, 10, 0.92);
|
|
245
|
-
color: #ffffff;
|
|
246
|
-
font-size: 11px;
|
|
247
|
-
font-weight: 500;
|
|
248
|
-
padding: 4px 8px;
|
|
249
|
-
border-radius: 4px;
|
|
250
|
-
white-space: nowrap;
|
|
251
|
-
pointer-events: none;
|
|
252
|
-
opacity: 0;
|
|
253
|
-
transform: translateY(-2px);
|
|
254
|
-
transition: opacity 130ms ease-out, transform 130ms ease-out;
|
|
255
|
-
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
|
|
256
|
-
max-width: 220px;
|
|
257
|
-
overflow: hidden;
|
|
258
|
-
text-overflow: ellipsis;
|
|
259
|
-
`;
|
|
260
|
-
return tip;
|
|
261
|
-
}
|
|
262
|
-
function createOverflowChip() {
|
|
263
|
-
const chip = document.createElement("div");
|
|
264
|
-
chip.setAttribute("data-vizu-presence-overflow", "");
|
|
265
|
-
chip.style.cssText = `
|
|
266
|
-
width: ${AVATAR_SIZE}px;
|
|
267
|
-
height: ${AVATAR_SIZE}px;
|
|
268
|
-
margin-left: -${OVERLAP_PX}px;
|
|
269
|
-
border-radius: 50%;
|
|
270
|
-
background: rgba(82, 82, 82, 0.95);
|
|
271
|
-
color: #ffffff;
|
|
272
|
-
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.95), 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
273
|
-
display: flex;
|
|
274
|
-
align-items: center;
|
|
275
|
-
justify-content: center;
|
|
276
|
-
font-size: 10.5px;
|
|
277
|
-
font-weight: 600;
|
|
278
|
-
letter-spacing: -0.02em;
|
|
279
|
-
line-height: 1;
|
|
280
|
-
user-select: none;
|
|
281
|
-
flex-shrink: 0;
|
|
282
|
-
`;
|
|
283
|
-
return chip;
|
|
284
|
-
}
|
|
285
|
-
function initialsOf(name) {
|
|
286
|
-
const trimmed = name.trim();
|
|
287
|
-
if (!trimmed) return "?";
|
|
288
|
-
const parts = trimmed.split(/\s+/);
|
|
289
|
-
if (parts.length >= 2 && parts[0] && parts[parts.length - 1]) {
|
|
290
|
-
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
291
|
-
}
|
|
292
|
-
return trimmed.slice(0, 2).toUpperCase();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// src/selection-overlay.ts
|
|
296
|
-
var Z_INDEX2 = 2147483639;
|
|
297
|
-
var SelectionOverlay = class {
|
|
298
|
-
constructor() {
|
|
299
|
-
this.container = null;
|
|
300
|
-
this.overlays = /* @__PURE__ */ new Map();
|
|
301
|
-
this.scrollListener = null;
|
|
302
|
-
this.resizeListener = null;
|
|
303
|
-
this.rafScheduled = false;
|
|
304
|
-
}
|
|
305
|
-
mount() {
|
|
306
|
-
if (typeof document === "undefined") return;
|
|
307
|
-
if (this.container) return;
|
|
308
|
-
this.container = document.createElement("div");
|
|
309
|
-
this.container.setAttribute("data-vizu-selections", "");
|
|
310
|
-
this.container.style.cssText = `
|
|
311
|
-
position: fixed; inset: 0;
|
|
312
|
-
pointer-events: none;
|
|
313
|
-
z-index: ${Z_INDEX2};
|
|
314
|
-
contain: strict;
|
|
315
|
-
`;
|
|
316
|
-
document.body.appendChild(this.container);
|
|
317
|
-
this.scrollListener = () => this.scheduleReposition();
|
|
318
|
-
this.resizeListener = () => this.scheduleReposition();
|
|
319
|
-
window.addEventListener("scroll", this.scrollListener, { passive: true, capture: true });
|
|
320
|
-
window.addEventListener("resize", this.resizeListener);
|
|
321
|
-
}
|
|
322
|
-
destroy() {
|
|
323
|
-
if (this.scrollListener) {
|
|
324
|
-
window.removeEventListener("scroll", this.scrollListener, true);
|
|
325
|
-
this.scrollListener = null;
|
|
326
|
-
}
|
|
327
|
-
if (this.resizeListener) {
|
|
328
|
-
window.removeEventListener("resize", this.resizeListener);
|
|
329
|
-
this.resizeListener = null;
|
|
330
|
-
}
|
|
331
|
-
for (const e of this.overlays.values()) e.el.remove();
|
|
332
|
-
this.overlays.clear();
|
|
333
|
-
this.container?.remove();
|
|
334
|
-
this.container = null;
|
|
335
|
-
}
|
|
336
|
-
setSelections(selections) {
|
|
337
|
-
if (!this.container) return;
|
|
338
|
-
const nextIds = new Set(selections.map((s) => s.id));
|
|
339
|
-
for (const [id, entry] of Array.from(this.overlays)) {
|
|
340
|
-
if (!nextIds.has(id)) {
|
|
341
|
-
entry.el.remove();
|
|
342
|
-
this.overlays.delete(id);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
for (const sel of selections) {
|
|
346
|
-
let entry = this.overlays.get(sel.id);
|
|
347
|
-
if (!entry) {
|
|
348
|
-
const el = createOverlay(sel.color, sel.name);
|
|
349
|
-
this.container.appendChild(el);
|
|
350
|
-
entry = { el, fingerprint: sel.fingerprint, color: sel.color, name: sel.name };
|
|
351
|
-
this.overlays.set(sel.id, entry);
|
|
352
|
-
} else {
|
|
353
|
-
entry.fingerprint = sel.fingerprint;
|
|
354
|
-
if (entry.color !== sel.color) {
|
|
355
|
-
entry.color = sel.color;
|
|
356
|
-
entry.el.style.borderColor = sel.color;
|
|
357
|
-
}
|
|
358
|
-
if (entry.name !== sel.name) {
|
|
359
|
-
entry.name = sel.name;
|
|
360
|
-
const label = entry.el.querySelector("[data-selection-label]");
|
|
361
|
-
if (label) {
|
|
362
|
-
label.textContent = sel.name;
|
|
363
|
-
label.style.background = sel.color;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
this.positionOverlay(entry);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
/* ────────────────── private ────────────────── */
|
|
371
|
-
positionOverlay(entry) {
|
|
372
|
-
const match = findByFingerprint(entry.fingerprint);
|
|
373
|
-
const target = match?.element ?? null;
|
|
374
|
-
if (!target) {
|
|
375
|
-
entry.el.style.display = "none";
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
const rect = target.getBoundingClientRect();
|
|
379
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
380
|
-
entry.el.style.display = "none";
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
entry.el.style.display = "block";
|
|
384
|
-
entry.el.style.left = `${rect.left}px`;
|
|
385
|
-
entry.el.style.top = `${rect.top}px`;
|
|
386
|
-
entry.el.style.width = `${rect.width}px`;
|
|
387
|
-
entry.el.style.height = `${rect.height}px`;
|
|
388
|
-
}
|
|
389
|
-
scheduleReposition() {
|
|
390
|
-
if (this.rafScheduled) return;
|
|
391
|
-
this.rafScheduled = true;
|
|
392
|
-
requestAnimationFrame(() => {
|
|
393
|
-
this.rafScheduled = false;
|
|
394
|
-
for (const entry of this.overlays.values()) this.positionOverlay(entry);
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
function createOverlay(color, name) {
|
|
399
|
-
const el = document.createElement("div");
|
|
400
|
-
el.setAttribute("data-vizu-selection-overlay", "");
|
|
401
|
-
const positionTransition = prefersReducedMotion() ? "none" : "left 90ms ease-out, top 90ms ease-out, width 90ms ease-out, height 90ms ease-out";
|
|
402
|
-
el.style.cssText = `
|
|
403
|
-
position: fixed;
|
|
404
|
-
pointer-events: none;
|
|
405
|
-
display: none;
|
|
406
|
-
border: 2px dashed ${color};
|
|
407
|
-
border-radius: 4px;
|
|
408
|
-
background: ${color}10;
|
|
409
|
-
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.85);
|
|
410
|
-
transition: ${positionTransition};
|
|
411
|
-
will-change: left, top, width, height;
|
|
412
|
-
`;
|
|
413
|
-
const label = document.createElement("div");
|
|
414
|
-
label.setAttribute("data-selection-label", "");
|
|
415
|
-
label.textContent = name;
|
|
416
|
-
label.style.cssText = `
|
|
417
|
-
position: absolute;
|
|
418
|
-
top: -22px;
|
|
419
|
-
left: -2px;
|
|
420
|
-
padding: 2px 7px;
|
|
421
|
-
background: ${color};
|
|
422
|
-
color: #ffffff;
|
|
423
|
-
font: 500 10.5px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
424
|
-
border-radius: 3px 3px 3px 0;
|
|
425
|
-
white-space: nowrap;
|
|
426
|
-
max-width: 180px;
|
|
427
|
-
overflow: hidden;
|
|
428
|
-
text-overflow: ellipsis;
|
|
429
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
|
|
430
|
-
`;
|
|
431
|
-
el.appendChild(label);
|
|
432
|
-
return el;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// src/live-collab.ts
|
|
436
|
-
function generateConnId() {
|
|
437
|
-
const a = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
438
|
-
const b = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
439
|
-
return `${a}${b}`;
|
|
440
|
-
}
|
|
441
|
-
var PUSH_INTERVAL_MS = 100;
|
|
442
|
-
var PULL_INTERVAL_MS = 200;
|
|
443
|
-
var INACTIVITY_FADE_MS = 5e3;
|
|
444
|
-
var CURSOR_TRANSITION_MS = 220;
|
|
445
|
-
var Z_INDEX3 = 2147483641;
|
|
446
|
-
var FOLLOW_SCROLL_THRESHOLD_PX = 8;
|
|
447
|
-
var RECONNECT_AFTER_ERRORS = 2;
|
|
448
|
-
var DISCONNECT_AFTER_ERRORS = 5;
|
|
449
|
-
function fnv1a32Hex(input) {
|
|
450
|
-
let h = 2166136261;
|
|
451
|
-
for (let i = 0; i < input.length; i++) {
|
|
452
|
-
h ^= input.charCodeAt(i);
|
|
453
|
-
h = Math.imul(h, 16777619);
|
|
454
|
-
}
|
|
455
|
-
return (h >>> 0).toString(16).padStart(8, "0");
|
|
456
|
-
}
|
|
457
|
-
function roomIdFor(workspaceSlug, pageUrl) {
|
|
458
|
-
return `ws_${workspaceSlug}__pg_${fnv1a32Hex(pageUrl)}`;
|
|
459
|
-
}
|
|
460
|
-
var LiveCollab = class {
|
|
461
|
-
constructor(opts) {
|
|
462
|
-
this.opts = opts;
|
|
463
|
-
this.container = null;
|
|
464
|
-
this.cursors = /* @__PURE__ */ new Map();
|
|
465
|
-
// keyed by connId now (was numeric connectionId)
|
|
466
|
-
this.presenceStack = null;
|
|
467
|
-
this.followPill = null;
|
|
468
|
-
this.selectionOverlay = null;
|
|
469
|
-
this.mouseListener = null;
|
|
470
|
-
this.scrollListener = null;
|
|
471
|
-
this.visibilityListener = null;
|
|
472
|
-
this.keydownListener = null;
|
|
473
|
-
this.statusIndicator = null;
|
|
474
|
-
this.pushTimer = null;
|
|
475
|
-
this.pullTimer = null;
|
|
476
|
-
this.destroyed = false;
|
|
477
|
-
/** Full room id, built once in start() from workspace + pageUrl. */
|
|
478
|
-
this.roomId = null;
|
|
479
|
-
/** Latest snapshot of other connections from /api/live/[room]/others. */
|
|
480
|
-
this.latestOthers = [];
|
|
481
|
-
/** Local presence — mouse + scroll + selection update this in place; pushTimer ships it. */
|
|
482
|
-
this.currentPresence = { cursor: null, scrollY: 0, selecting: null };
|
|
483
|
-
/** True when something changed since the last push — skip the POST otherwise. */
|
|
484
|
-
this.presenceDirty = false;
|
|
485
|
-
/** Connection-health tracker for the indicator. */
|
|
486
|
-
this.status = "initial";
|
|
487
|
-
this.consecutiveErrors = 0;
|
|
488
|
-
// Follow mode state
|
|
489
|
-
/** Clerk user id we're currently following, or null. */
|
|
490
|
-
this.followingUserId = null;
|
|
491
|
-
/** Last scrollY we received from the followee — for delta gating. */
|
|
492
|
-
this.lastFolloweeScrollY = null;
|
|
493
|
-
this.connId = generateConnId();
|
|
494
|
-
if (typeof window !== "undefined") {
|
|
495
|
-
this.currentPresence.scrollY = window.scrollY;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
async start() {
|
|
499
|
-
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
500
|
-
if (this.destroyed) return;
|
|
501
|
-
try {
|
|
502
|
-
const pageUrl = this.opts.pageUrl ?? window.location.origin + window.location.pathname;
|
|
503
|
-
this.roomId = roomIdFor(this.opts.workspaceSlug, pageUrl);
|
|
504
|
-
this.mountContainer();
|
|
505
|
-
this.presenceStack = new PresenceStack({
|
|
506
|
-
onAvatarClick: (userId) => this.setFollowing(userId)
|
|
507
|
-
});
|
|
508
|
-
this.presenceStack.mount();
|
|
509
|
-
this.followPill = new FollowPill({
|
|
510
|
-
onStop: () => this.setFollowing(null)
|
|
511
|
-
});
|
|
512
|
-
this.selectionOverlay = new SelectionOverlay();
|
|
513
|
-
this.selectionOverlay.mount();
|
|
514
|
-
this.statusIndicator = new ConnectionIndicator();
|
|
515
|
-
this.statusIndicator.mount();
|
|
516
|
-
this.setStatus("connecting");
|
|
517
|
-
this.attachMouseListener();
|
|
518
|
-
this.attachScrollListener();
|
|
519
|
-
this.attachVisibilityListener();
|
|
520
|
-
this.attachKeyboardListener();
|
|
521
|
-
this.pushTimer = window.setInterval(() => void this.push(), PUSH_INTERVAL_MS);
|
|
522
|
-
this.pullTimer = window.setInterval(() => void this.pull(), PULL_INTERVAL_MS);
|
|
523
|
-
this.presenceDirty = true;
|
|
524
|
-
void this.push();
|
|
525
|
-
void this.pull();
|
|
526
|
-
} catch (err) {
|
|
527
|
-
if (typeof console !== "undefined") {
|
|
528
|
-
console.warn("[vizu/live-collab] start failed; live cursors disabled:", err);
|
|
529
|
-
}
|
|
530
|
-
this.cleanup();
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
destroy() {
|
|
534
|
-
this.destroyed = true;
|
|
535
|
-
this.cleanup();
|
|
536
|
-
}
|
|
537
|
-
/* ────────────────── private ────────────────── */
|
|
538
|
-
cleanup() {
|
|
539
|
-
void this.clearRemotePresence();
|
|
540
|
-
if (this.mouseListener) {
|
|
541
|
-
window.removeEventListener("mousemove", this.mouseListener);
|
|
542
|
-
this.mouseListener = null;
|
|
543
|
-
}
|
|
544
|
-
if (this.scrollListener) {
|
|
545
|
-
window.removeEventListener("scroll", this.scrollListener);
|
|
546
|
-
this.scrollListener = null;
|
|
547
|
-
}
|
|
548
|
-
if (this.visibilityListener) {
|
|
549
|
-
document.removeEventListener("visibilitychange", this.visibilityListener);
|
|
550
|
-
this.visibilityListener = null;
|
|
551
|
-
}
|
|
552
|
-
if (this.keydownListener) {
|
|
553
|
-
document.removeEventListener("keydown", this.keydownListener);
|
|
554
|
-
this.keydownListener = null;
|
|
555
|
-
}
|
|
556
|
-
if (this.pushTimer != null) {
|
|
557
|
-
window.clearInterval(this.pushTimer);
|
|
558
|
-
this.pushTimer = null;
|
|
559
|
-
}
|
|
560
|
-
if (this.pullTimer != null) {
|
|
561
|
-
window.clearInterval(this.pullTimer);
|
|
562
|
-
this.pullTimer = null;
|
|
563
|
-
}
|
|
564
|
-
for (const c of this.cursors.values()) {
|
|
565
|
-
if (c.fadeTimer != null) window.clearTimeout(c.fadeTimer);
|
|
566
|
-
c.el.remove();
|
|
567
|
-
}
|
|
568
|
-
this.cursors.clear();
|
|
569
|
-
this.presenceStack?.destroy();
|
|
570
|
-
this.presenceStack = null;
|
|
571
|
-
this.followPill?.destroy();
|
|
572
|
-
this.followPill = null;
|
|
573
|
-
this.selectionOverlay?.destroy();
|
|
574
|
-
this.selectionOverlay = null;
|
|
575
|
-
this.statusIndicator?.destroy();
|
|
576
|
-
this.statusIndicator = null;
|
|
577
|
-
this.followingUserId = null;
|
|
578
|
-
this.lastFolloweeScrollY = null;
|
|
579
|
-
this.container?.remove();
|
|
580
|
-
this.container = null;
|
|
581
|
-
this.roomId = null;
|
|
582
|
-
this.latestOthers = [];
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Public API: broadcast the local user's currently-hovered element
|
|
586
|
-
* (highlighter mode) so other clients can render a ghost outline on
|
|
587
|
-
* it. Pass null to clear (highlighter stopped, popover opened, etc.).
|
|
588
|
-
* Idempotent on no-op transitions. Slice 5 of live-collab.md.
|
|
589
|
-
*/
|
|
590
|
-
setLocalSelection(fingerprint) {
|
|
591
|
-
this.currentPresence.selecting = fingerprint ? { fingerprintJson: JSON.stringify(fingerprint) } : null;
|
|
592
|
-
this.presenceDirty = true;
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Public API: start/stop following a user by Clerk id. Null clears.
|
|
596
|
-
* Called by the avatar click handler and by the FollowPill stop button.
|
|
597
|
-
* Idempotent — same id again is a no-op.
|
|
598
|
-
*/
|
|
599
|
-
setFollowing(userId) {
|
|
600
|
-
if (this.followingUserId === userId) return;
|
|
601
|
-
this.followingUserId = userId;
|
|
602
|
-
this.lastFolloweeScrollY = null;
|
|
603
|
-
this.presenceStack?.setFollowing(userId);
|
|
604
|
-
this.followPill?.setFollowing(userId ? this.lookupUserForPill(userId) : null);
|
|
605
|
-
if (userId) this.scrollToFolloweeIfKnown();
|
|
606
|
-
}
|
|
607
|
-
lookupUserForPill(userId) {
|
|
608
|
-
for (const o of this.latestOthers) {
|
|
609
|
-
if (o.user.userId === userId) {
|
|
610
|
-
return {
|
|
611
|
-
id: userId,
|
|
612
|
-
name: o.user.name ?? "Anonymous",
|
|
613
|
-
color: colorForUser(userId),
|
|
614
|
-
avatar: o.user.avatar
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return null;
|
|
619
|
-
}
|
|
620
|
-
scrollToFolloweeIfKnown() {
|
|
621
|
-
if (!this.followingUserId) return;
|
|
622
|
-
for (const o of this.latestOthers) {
|
|
623
|
-
if (o.user.userId === this.followingUserId) {
|
|
624
|
-
if (typeof o.presence.scrollY === "number") {
|
|
625
|
-
this.followScrollTo(o.presence.scrollY);
|
|
626
|
-
}
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
followScrollTo(scrollY) {
|
|
632
|
-
if (this.lastFolloweeScrollY !== null && Math.abs(this.lastFolloweeScrollY - scrollY) < FOLLOW_SCROLL_THRESHOLD_PX) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
this.lastFolloweeScrollY = scrollY;
|
|
636
|
-
window.scrollTo({ top: scrollY, behavior: "smooth" });
|
|
637
|
-
}
|
|
638
|
-
mountContainer() {
|
|
639
|
-
this.container = document.createElement("div");
|
|
640
|
-
this.container.setAttribute("data-vizu-live-cursors", "");
|
|
641
|
-
this.container.style.cssText = `
|
|
642
|
-
position: fixed; inset: 0;
|
|
643
|
-
pointer-events: none;
|
|
644
|
-
z-index: ${Z_INDEX3};
|
|
645
|
-
contain: strict;
|
|
646
|
-
`;
|
|
647
|
-
document.body.appendChild(this.container);
|
|
648
|
-
}
|
|
649
|
-
attachMouseListener() {
|
|
650
|
-
this.mouseListener = (e) => {
|
|
651
|
-
this.currentPresence.cursor = {
|
|
652
|
-
xPct: e.clientX / window.innerWidth * 100,
|
|
653
|
-
yPct: e.clientY / window.innerHeight * 100
|
|
654
|
-
};
|
|
655
|
-
this.presenceDirty = true;
|
|
656
|
-
};
|
|
657
|
-
window.addEventListener("mousemove", this.mouseListener, { passive: true });
|
|
658
|
-
}
|
|
659
|
-
attachScrollListener() {
|
|
660
|
-
this.scrollListener = () => {
|
|
661
|
-
this.currentPresence.scrollY = window.scrollY;
|
|
662
|
-
this.presenceDirty = true;
|
|
663
|
-
};
|
|
664
|
-
window.addEventListener("scroll", this.scrollListener, { passive: true });
|
|
665
|
-
}
|
|
666
|
-
attachVisibilityListener() {
|
|
667
|
-
this.visibilityListener = () => {
|
|
668
|
-
if (document.hidden) {
|
|
669
|
-
void this.clearRemotePresence();
|
|
670
|
-
} else {
|
|
671
|
-
this.presenceDirty = true;
|
|
672
|
-
}
|
|
673
|
-
};
|
|
674
|
-
document.addEventListener("visibilitychange", this.visibilityListener);
|
|
675
|
-
}
|
|
676
|
-
attachKeyboardListener() {
|
|
677
|
-
this.keydownListener = (e) => {
|
|
678
|
-
if (e.key !== "Escape") return;
|
|
679
|
-
if (!this.followingUserId) return;
|
|
680
|
-
this.setFollowing(null);
|
|
681
|
-
};
|
|
682
|
-
document.addEventListener("keydown", this.keydownListener);
|
|
683
|
-
}
|
|
684
|
-
setStatus(next) {
|
|
685
|
-
if (this.status === next) return;
|
|
686
|
-
this.status = next;
|
|
687
|
-
this.statusIndicator?.setStatus(next);
|
|
688
|
-
}
|
|
689
|
-
/**
|
|
690
|
-
* Push tick — POST current presence to /api/live/[room]/me. Skips
|
|
691
|
-
* the request when nothing changed since last push, so an idle tab
|
|
692
|
-
* generates near-zero traffic.
|
|
693
|
-
*/
|
|
694
|
-
async push() {
|
|
695
|
-
if (this.destroyed || !this.roomId) return;
|
|
696
|
-
if (!this.presenceDirty) return;
|
|
697
|
-
if (typeof document !== "undefined" && document.hidden) return;
|
|
698
|
-
this.presenceDirty = false;
|
|
699
|
-
try {
|
|
700
|
-
const res = await this.fetchLive(`/api/live/${encodeURIComponent(this.roomId)}/me`, {
|
|
701
|
-
method: "POST",
|
|
702
|
-
body: JSON.stringify({
|
|
703
|
-
connId: this.connId,
|
|
704
|
-
presence: this.currentPresence
|
|
705
|
-
})
|
|
706
|
-
});
|
|
707
|
-
if (!res.ok) {
|
|
708
|
-
if (res.status === 402 || res.status === 401 || res.status === 503) {
|
|
709
|
-
this.setStatus("disconnected");
|
|
710
|
-
if (this.pushTimer != null) {
|
|
711
|
-
window.clearInterval(this.pushTimer);
|
|
712
|
-
this.pushTimer = null;
|
|
713
|
-
}
|
|
714
|
-
if (this.pullTimer != null) {
|
|
715
|
-
window.clearInterval(this.pullTimer);
|
|
716
|
-
this.pullTimer = null;
|
|
717
|
-
}
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
throw new Error(`push HTTP ${res.status}`);
|
|
721
|
-
}
|
|
722
|
-
this.noteSuccess();
|
|
723
|
-
} catch (err) {
|
|
724
|
-
this.noteError(err);
|
|
725
|
-
this.presenceDirty = true;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
/**
|
|
729
|
-
* Pull tick — GET /api/live/[room]/others and drive cursor + stack +
|
|
730
|
-
* selection + follow updates. Same handler shape as the Liveblocks-era
|
|
731
|
-
* subscribeOthers callback; only the data source changed.
|
|
732
|
-
*/
|
|
733
|
-
async pull() {
|
|
734
|
-
if (this.destroyed || !this.roomId) return;
|
|
735
|
-
try {
|
|
736
|
-
const res = await this.fetchLive(
|
|
737
|
-
`/api/live/${encodeURIComponent(this.roomId)}/others?me=${encodeURIComponent(this.connId)}`,
|
|
738
|
-
{ method: "GET" }
|
|
739
|
-
);
|
|
740
|
-
if (!res.ok) {
|
|
741
|
-
if (res.status === 503 || res.status === 401) {
|
|
742
|
-
this.setStatus("disconnected");
|
|
743
|
-
if (this.pullTimer != null) {
|
|
744
|
-
window.clearInterval(this.pullTimer);
|
|
745
|
-
this.pullTimer = null;
|
|
746
|
-
}
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
throw new Error(`pull HTTP ${res.status}`);
|
|
750
|
-
}
|
|
751
|
-
const body = await res.json();
|
|
752
|
-
const others = (body.others ?? []).map((o) => ({
|
|
753
|
-
connId: o.connId,
|
|
754
|
-
presence: o.payload.presence,
|
|
755
|
-
user: o.payload.user
|
|
756
|
-
}));
|
|
757
|
-
this.latestOthers = others;
|
|
758
|
-
this.renderOthers(others);
|
|
759
|
-
this.noteSuccess();
|
|
760
|
-
} catch (err) {
|
|
761
|
-
this.noteError(err);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
noteSuccess() {
|
|
765
|
-
this.consecutiveErrors = 0;
|
|
766
|
-
if (this.status !== "connected") this.setStatus("connected");
|
|
767
|
-
}
|
|
768
|
-
noteError(err) {
|
|
769
|
-
this.consecutiveErrors++;
|
|
770
|
-
if (this.consecutiveErrors >= DISCONNECT_AFTER_ERRORS) {
|
|
771
|
-
this.setStatus("disconnected");
|
|
772
|
-
} else if (this.consecutiveErrors >= RECONNECT_AFTER_ERRORS) {
|
|
773
|
-
this.setStatus("reconnecting");
|
|
774
|
-
}
|
|
775
|
-
if (typeof console !== "undefined" && this.consecutiveErrors === 1) {
|
|
776
|
-
console.warn("[vizu/live-collab] transport error", err);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Drive the cursor / stack / selection / follow layers from a fresh
|
|
781
|
-
* `others` snapshot. Mirrors the body of the Liveblocks-era
|
|
782
|
-
* subscribeOthers callback so DOM behavior is unchanged.
|
|
783
|
-
*/
|
|
784
|
-
renderOthers(others) {
|
|
785
|
-
const seen = /* @__PURE__ */ new Set();
|
|
786
|
-
const stackUsers = /* @__PURE__ */ new Map();
|
|
787
|
-
const selections = [];
|
|
788
|
-
let followeeScrollY = null;
|
|
789
|
-
let followeeStillPresent = false;
|
|
790
|
-
for (const other of others) {
|
|
791
|
-
seen.add(other.connId);
|
|
792
|
-
const userId = other.user.userId;
|
|
793
|
-
const name = other.user.name ?? "Anonymous";
|
|
794
|
-
const color = colorForUser(userId);
|
|
795
|
-
if (!stackUsers.has(userId)) {
|
|
796
|
-
stackUsers.set(userId, { id: userId, name, color, avatar: other.user.avatar });
|
|
797
|
-
}
|
|
798
|
-
if (this.followingUserId === userId && followeeScrollY === null) {
|
|
799
|
-
followeeStillPresent = true;
|
|
800
|
-
if (typeof other.presence.scrollY === "number") {
|
|
801
|
-
followeeScrollY = other.presence.scrollY;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
if (other.presence.selecting && !selections.find((s) => s.id === userId)) {
|
|
805
|
-
try {
|
|
806
|
-
const fp = JSON.parse(other.presence.selecting.fingerprintJson);
|
|
807
|
-
selections.push({ id: userId, name, color, fingerprint: fp });
|
|
808
|
-
} catch {
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
if (!other.presence.cursor) {
|
|
812
|
-
this.removeCursor(other.connId);
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
this.upsertCursor(other.connId, userId, name, other.presence.cursor);
|
|
816
|
-
}
|
|
817
|
-
for (const id of Array.from(this.cursors.keys())) {
|
|
818
|
-
if (!seen.has(id)) this.removeCursor(id);
|
|
819
|
-
}
|
|
820
|
-
this.presenceStack?.setOthers(Array.from(stackUsers.values()));
|
|
821
|
-
this.selectionOverlay?.setSelections(selections);
|
|
822
|
-
if (this.followingUserId) {
|
|
823
|
-
if (!followeeStillPresent) {
|
|
824
|
-
this.setFollowing(null);
|
|
825
|
-
} else if (followeeScrollY !== null) {
|
|
826
|
-
this.followScrollTo(followeeScrollY);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
/**
|
|
831
|
-
* Async wrapper around fetch that injects the optional auth header
|
|
832
|
-
* resolved from opts.getAuthHeader. Same-origin requests rely on
|
|
833
|
-
* cookies via `credentials: 'include'`; cross-origin gets the
|
|
834
|
-
* bearer token returned by the host.
|
|
835
|
-
*/
|
|
836
|
-
async fetchLive(path, init) {
|
|
837
|
-
const headers = new Headers(init.headers);
|
|
838
|
-
headers.set("content-type", "application/json");
|
|
839
|
-
if (this.opts.getAuthHeader) {
|
|
840
|
-
const extra = await this.opts.getAuthHeader();
|
|
841
|
-
if (extra) {
|
|
842
|
-
new Headers(extra).forEach((v, k) => headers.set(k, v));
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
return fetch(this.opts.apiUrl + path, {
|
|
846
|
-
...init,
|
|
847
|
-
headers,
|
|
848
|
-
credentials: "include"
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Fire-and-forget clearOnly POST so other clients see us drop within
|
|
853
|
-
* one pull cycle (~200ms) instead of waiting for the 5s presence TTL.
|
|
854
|
-
* Errors swallowed — the TTL is the safety net.
|
|
855
|
-
*/
|
|
856
|
-
async clearRemotePresence() {
|
|
857
|
-
if (!this.roomId) return;
|
|
858
|
-
try {
|
|
859
|
-
await this.fetchLive(`/api/live/${encodeURIComponent(this.roomId)}/me`, {
|
|
860
|
-
method: "POST",
|
|
861
|
-
body: JSON.stringify({ connId: this.connId, clearOnly: true })
|
|
862
|
-
});
|
|
863
|
-
} catch {
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
upsertCursor(connectionId, userId, name, cursor) {
|
|
867
|
-
if (!this.container) return;
|
|
868
|
-
let entry = this.cursors.get(connectionId);
|
|
869
|
-
if (!entry) {
|
|
870
|
-
const color = colorForUser(userId);
|
|
871
|
-
const el = createCursorElement(name, color);
|
|
872
|
-
this.container.appendChild(el);
|
|
873
|
-
entry = { el, fadeTimer: null };
|
|
874
|
-
this.cursors.set(connectionId, entry);
|
|
875
|
-
}
|
|
876
|
-
const inViewport = cursor.xPct >= 0 && cursor.xPct <= 100 && cursor.yPct >= 0 && cursor.yPct <= 100;
|
|
877
|
-
if (!inViewport) {
|
|
878
|
-
entry.el.style.opacity = "0";
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
const x = window.innerWidth * cursor.xPct / 100;
|
|
882
|
-
const y = window.innerHeight * cursor.yPct / 100;
|
|
883
|
-
entry.el.style.transform = `translate(${x}px, ${y}px)`;
|
|
884
|
-
entry.el.style.opacity = "1";
|
|
885
|
-
if (entry.fadeTimer != null) window.clearTimeout(entry.fadeTimer);
|
|
886
|
-
entry.fadeTimer = window.setTimeout(() => {
|
|
887
|
-
if (entry) entry.el.style.opacity = "0";
|
|
888
|
-
}, INACTIVITY_FADE_MS);
|
|
889
|
-
}
|
|
890
|
-
removeCursor(connectionId) {
|
|
891
|
-
const entry = this.cursors.get(connectionId);
|
|
892
|
-
if (!entry) return;
|
|
893
|
-
if (entry.fadeTimer != null) window.clearTimeout(entry.fadeTimer);
|
|
894
|
-
entry.el.style.opacity = "0";
|
|
895
|
-
window.setTimeout(() => entry.el.remove(), 220);
|
|
896
|
-
this.cursors.delete(connectionId);
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
function createCursorElement(name, color) {
|
|
900
|
-
const wrap = document.createElement("div");
|
|
901
|
-
wrap.setAttribute("data-vizu-cursor", "");
|
|
902
|
-
const transformTransition = prefersReducedMotion() ? "none" : `transform ${CURSOR_TRANSITION_MS}ms cubic-bezier(0.16, 1, 0.3, 1)`;
|
|
903
|
-
wrap.style.cssText = `
|
|
904
|
-
position: absolute; top: 0; left: 0;
|
|
905
|
-
pointer-events: none;
|
|
906
|
-
opacity: 0;
|
|
907
|
-
transform: translate(0px, 0px);
|
|
908
|
-
transition: ${transformTransition}, opacity 220ms ease-out;
|
|
909
|
-
will-change: transform;
|
|
910
|
-
`;
|
|
911
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
912
|
-
svg.setAttribute("width", "18");
|
|
913
|
-
svg.setAttribute("height", "20");
|
|
914
|
-
svg.setAttribute("viewBox", "0 0 16 18");
|
|
915
|
-
svg.style.cssText = "display: block; filter: drop-shadow(0 1px 1.5px rgba(0,0,0,0.3));";
|
|
916
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
917
|
-
path.setAttribute("d", "M0 0 L0 13 L4 9 L7 16 L9 15 L6 8 L11 8 Z");
|
|
918
|
-
path.setAttribute("fill", color);
|
|
919
|
-
path.setAttribute("stroke", "#ffffff");
|
|
920
|
-
path.setAttribute("stroke-width", "1.2");
|
|
921
|
-
path.setAttribute("stroke-linejoin", "round");
|
|
922
|
-
svg.appendChild(path);
|
|
923
|
-
wrap.appendChild(svg);
|
|
924
|
-
const label = document.createElement("div");
|
|
925
|
-
label.textContent = name;
|
|
926
|
-
label.style.cssText = `
|
|
927
|
-
margin-top: 2px;
|
|
928
|
-
margin-left: 10px;
|
|
929
|
-
padding: 2px 7px;
|
|
930
|
-
background: ${color};
|
|
931
|
-
color: #ffffff;
|
|
932
|
-
font: 500 11px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
933
|
-
border-radius: 4px;
|
|
934
|
-
white-space: nowrap;
|
|
935
|
-
box-shadow: 0 2px 6px rgba(0,0,0,0.18);
|
|
936
|
-
max-width: 160px;
|
|
937
|
-
overflow: hidden;
|
|
938
|
-
text-overflow: ellipsis;
|
|
939
|
-
`;
|
|
940
|
-
wrap.appendChild(label);
|
|
941
|
-
return wrap;
|
|
942
|
-
}
|
|
943
|
-
var ConnectionIndicator = class {
|
|
944
|
-
constructor() {
|
|
945
|
-
this.el = null;
|
|
946
|
-
this.label = null;
|
|
947
|
-
}
|
|
948
|
-
mount() {
|
|
949
|
-
if (typeof document === "undefined") return;
|
|
950
|
-
if (this.el) return;
|
|
951
|
-
this.el = document.createElement("div");
|
|
952
|
-
this.el.setAttribute("data-vizu-connection-status", "");
|
|
953
|
-
this.el.style.cssText = `
|
|
954
|
-
position: fixed;
|
|
955
|
-
top: 18px;
|
|
956
|
-
right: 200px;
|
|
957
|
-
z-index: ${Z_INDEX3};
|
|
958
|
-
display: none;
|
|
959
|
-
align-items: center;
|
|
960
|
-
gap: 7px;
|
|
961
|
-
padding: 4px 10px 4px 8px;
|
|
962
|
-
background: rgba(10, 10, 10, 0.88);
|
|
963
|
-
color: #fafafa;
|
|
964
|
-
font: 500 11px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
965
|
-
border-radius: 999px;
|
|
966
|
-
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
|
|
967
|
-
pointer-events: none;
|
|
968
|
-
`;
|
|
969
|
-
const dot = document.createElement("span");
|
|
970
|
-
dot.style.cssText = `
|
|
971
|
-
display: inline-block;
|
|
972
|
-
width: 6px; height: 6px;
|
|
973
|
-
border-radius: 50%;
|
|
974
|
-
background: #F59E0B;
|
|
975
|
-
box-shadow: 0 0 6px #F59E0B;
|
|
976
|
-
`;
|
|
977
|
-
const label = document.createElement("span");
|
|
978
|
-
this.label = label;
|
|
979
|
-
this.el.appendChild(dot);
|
|
980
|
-
this.el.appendChild(label);
|
|
981
|
-
document.body.appendChild(this.el);
|
|
982
|
-
}
|
|
983
|
-
setStatus(status) {
|
|
984
|
-
if (!this.el || !this.label) return;
|
|
985
|
-
if (status === "connected") {
|
|
986
|
-
this.el.style.display = "none";
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
this.label.textContent = labelForStatus(status);
|
|
990
|
-
this.el.style.display = "inline-flex";
|
|
991
|
-
}
|
|
992
|
-
destroy() {
|
|
993
|
-
this.el?.remove();
|
|
994
|
-
this.el = null;
|
|
995
|
-
this.label = null;
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
|
-
function labelForStatus(status) {
|
|
999
|
-
switch (status) {
|
|
1000
|
-
case "connecting":
|
|
1001
|
-
return "Connecting\u2026";
|
|
1002
|
-
case "reconnecting":
|
|
1003
|
-
return "Reconnecting\u2026";
|
|
1004
|
-
case "disconnected":
|
|
1005
|
-
return "Offline";
|
|
1006
|
-
case "initial":
|
|
1007
|
-
return "Connecting\u2026";
|
|
1008
|
-
case "connected":
|
|
1009
|
-
return "";
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
var FollowPill = class {
|
|
1013
|
-
constructor(opts) {
|
|
1014
|
-
this.el = null;
|
|
1015
|
-
this.onStop = opts.onStop;
|
|
1016
|
-
}
|
|
1017
|
-
setFollowing(user) {
|
|
1018
|
-
if (typeof document === "undefined") return;
|
|
1019
|
-
if (!user) {
|
|
1020
|
-
this.el?.remove();
|
|
1021
|
-
this.el = null;
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
if (!this.el) this.el = this.mount();
|
|
1025
|
-
this.paint(user);
|
|
1026
|
-
}
|
|
1027
|
-
destroy() {
|
|
1028
|
-
this.el?.remove();
|
|
1029
|
-
this.el = null;
|
|
1030
|
-
}
|
|
1031
|
-
mount() {
|
|
1032
|
-
const pill = document.createElement("div");
|
|
1033
|
-
pill.setAttribute("data-vizu-follow-pill", "");
|
|
1034
|
-
pill.style.cssText = `
|
|
1035
|
-
position: fixed;
|
|
1036
|
-
top: 56px;
|
|
1037
|
-
right: 16px;
|
|
1038
|
-
z-index: ${Z_INDEX3};
|
|
1039
|
-
display: inline-flex;
|
|
1040
|
-
align-items: center;
|
|
1041
|
-
gap: 8px;
|
|
1042
|
-
padding: 6px 6px 6px 12px;
|
|
1043
|
-
background: rgba(10, 10, 10, 0.92);
|
|
1044
|
-
color: #ffffff;
|
|
1045
|
-
border-radius: 999px;
|
|
1046
|
-
font: 500 12px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
1047
|
-
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
|
|
1048
|
-
pointer-events: auto;
|
|
1049
|
-
`;
|
|
1050
|
-
document.body.appendChild(pill);
|
|
1051
|
-
return pill;
|
|
1052
|
-
}
|
|
1053
|
-
paint(user) {
|
|
1054
|
-
if (!this.el) return;
|
|
1055
|
-
this.el.textContent = "";
|
|
1056
|
-
const dot = document.createElement("span");
|
|
1057
|
-
dot.style.cssText = `
|
|
1058
|
-
display: inline-block;
|
|
1059
|
-
width: 8px; height: 8px;
|
|
1060
|
-
border-radius: 50%;
|
|
1061
|
-
background: ${user.color};
|
|
1062
|
-
box-shadow: 0 0 8px ${user.color};
|
|
1063
|
-
`;
|
|
1064
|
-
this.el.appendChild(dot);
|
|
1065
|
-
const label = document.createElement("span");
|
|
1066
|
-
label.textContent = `Following ${user.name}`;
|
|
1067
|
-
label.style.cssText = "max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
|
|
1068
|
-
this.el.appendChild(label);
|
|
1069
|
-
const stop = document.createElement("button");
|
|
1070
|
-
stop.type = "button";
|
|
1071
|
-
stop.setAttribute("aria-label", `Stop following ${user.name}`);
|
|
1072
|
-
stop.innerHTML = '<svg width="11" height="11" viewBox="0 0 12 12" aria-hidden="true"><path d="M1 1 L11 11 M11 1 L1 11" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" /></svg>';
|
|
1073
|
-
stop.style.cssText = `
|
|
1074
|
-
display: inline-flex;
|
|
1075
|
-
align-items: center;
|
|
1076
|
-
justify-content: center;
|
|
1077
|
-
width: 22px; height: 22px;
|
|
1078
|
-
border: none;
|
|
1079
|
-
background: rgba(255, 255, 255, 0.12);
|
|
1080
|
-
color: #ffffff;
|
|
1081
|
-
border-radius: 50%;
|
|
1082
|
-
cursor: pointer;
|
|
1083
|
-
transition: background 120ms ease-out;
|
|
1084
|
-
`;
|
|
1085
|
-
stop.addEventListener("mouseenter", () => stop.style.background = "rgba(255, 255, 255, 0.22)");
|
|
1086
|
-
stop.addEventListener("mouseleave", () => stop.style.background = "rgba(255, 255, 255, 0.12)");
|
|
1087
|
-
stop.addEventListener("click", (e) => {
|
|
1088
|
-
e.preventDefault();
|
|
1089
|
-
this.onStop();
|
|
1090
|
-
});
|
|
1091
|
-
this.el.appendChild(stop);
|
|
1092
|
-
}
|
|
1093
|
-
};
|
|
1094
|
-
export {
|
|
1095
|
-
LiveCollab,
|
|
1096
|
-
roomIdFor
|
|
1097
|
-
};
|
|
1098
|
-
//# sourceMappingURL=live-collab-BVLNJ5QI.js.map
|