canvu-react 0.3.5

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/camera-BwQjm5oh.d.cts +50 -0
  4. package/dist/camera-KwCYYPhm.d.ts +50 -0
  5. package/dist/chatbot.cjs +221 -0
  6. package/dist/chatbot.cjs.map +1 -0
  7. package/dist/chatbot.d.cts +36 -0
  8. package/dist/chatbot.d.ts +36 -0
  9. package/dist/chatbot.js +218 -0
  10. package/dist/chatbot.js.map +1 -0
  11. package/dist/index.cjs +1920 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.d.cts +276 -0
  14. package/dist/index.d.ts +276 -0
  15. package/dist/index.js +1867 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/native.cjs +2572 -0
  18. package/dist/native.cjs.map +1 -0
  19. package/dist/native.d.cts +217 -0
  20. package/dist/native.d.ts +217 -0
  21. package/dist/native.js +2562 -0
  22. package/dist/native.js.map +1 -0
  23. package/dist/react.cjs +8540 -0
  24. package/dist/react.cjs.map +1 -0
  25. package/dist/react.d.cts +481 -0
  26. package/dist/react.d.ts +481 -0
  27. package/dist/react.js +8492 -0
  28. package/dist/react.js.map +1 -0
  29. package/dist/realtime.cjs +2338 -0
  30. package/dist/realtime.cjs.map +1 -0
  31. package/dist/realtime.d.cts +309 -0
  32. package/dist/realtime.d.ts +309 -0
  33. package/dist/realtime.js +2317 -0
  34. package/dist/realtime.js.map +1 -0
  35. package/dist/shape-builders-DTYvub8W.d.ts +93 -0
  36. package/dist/shape-builders-DxPoOecg.d.cts +93 -0
  37. package/dist/tldraw.cjs +1948 -0
  38. package/dist/tldraw.cjs.map +1 -0
  39. package/dist/tldraw.d.cts +98 -0
  40. package/dist/tldraw.d.ts +98 -0
  41. package/dist/tldraw.js +1941 -0
  42. package/dist/tldraw.js.map +1 -0
  43. package/dist/types--ALu1mF-.d.ts +356 -0
  44. package/dist/types-B58i5k-u.d.cts +35 -0
  45. package/dist/types-CB0TZZuk.d.cts +157 -0
  46. package/dist/types-CB0TZZuk.d.ts +157 -0
  47. package/dist/types-D1ftVsOQ.d.cts +356 -0
  48. package/dist/types-DgEArHkA.d.ts +35 -0
  49. package/package.json +103 -0
@@ -0,0 +1,2317 @@
1
+ import { MousePointer2, MessageSquare, Sparkles, Hand, Square, Circle, Minus, ArrowUpRight, PenLine, Highlighter, Eraser, Type, Image } from 'lucide-react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { createContext, useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect, useContext } from 'react';
4
+
5
+ // src/react/presence/map-placement-preview.ts
6
+ function remoteMarkupStrokeFromPlacementPreview(preview) {
7
+ if (!preview || preview.kind !== "stroke" || preview.points.length === 0) {
8
+ return null;
9
+ }
10
+ const tool = preview.tool;
11
+ const mapped = tool === "laser" || tool === "marker" || tool === "draw" ? tool : "draw";
12
+ return {
13
+ points: preview.points,
14
+ tool: mapped
15
+ };
16
+ }
17
+
18
+ // src/math/rect.ts
19
+ function normalizeRect(r) {
20
+ const x0 = r.width >= 0 ? r.x : r.x + r.width;
21
+ const y0 = r.height >= 0 ? r.y : r.y + r.height;
22
+ return {
23
+ x: x0,
24
+ y: y0,
25
+ width: Math.abs(r.width),
26
+ height: Math.abs(r.height)
27
+ };
28
+ }
29
+
30
+ // src/renderer/svg-vector-renderer.ts
31
+ function formatCameraTransform(camera) {
32
+ const z = camera.zoom;
33
+ return `matrix(${z}, 0, 0, ${z}, ${camera.x}, ${camera.y})`;
34
+ }
35
+
36
+ // src/scene/freehand-path.ts
37
+ function smoothFreehandPointsToPathD(points) {
38
+ const n = points.length;
39
+ if (n === 0) return "";
40
+ if (n === 1) {
41
+ const p = points[0];
42
+ if (!p) return "";
43
+ return `M ${p.x} ${p.y}`;
44
+ }
45
+ if (n === 2) {
46
+ const a = points[0];
47
+ const b = points[1];
48
+ if (!a || !b) return "";
49
+ return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
50
+ }
51
+ const p0 = points[0];
52
+ if (!p0) return "";
53
+ let d = `M ${p0.x} ${p0.y}`;
54
+ let i = 1;
55
+ for (; i < n - 2; i++) {
56
+ const pi = points[i];
57
+ const pi1 = points[i + 1];
58
+ if (!pi || !pi1) continue;
59
+ const xc = (pi.x + pi1.x) / 2;
60
+ const yc = (pi.y + pi1.y) / 2;
61
+ d += ` Q ${pi.x} ${pi.y} ${xc} ${yc}`;
62
+ }
63
+ const pLast = points[i];
64
+ const pEnd = points[i + 1];
65
+ if (!pLast || !pEnd) return d;
66
+ d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
67
+ return d;
68
+ }
69
+
70
+ // src/react/presence/peer-color.ts
71
+ function defaultPresenceColorForId(id) {
72
+ let h = 2166136261;
73
+ for (let i = 0; i < id.length; i++) {
74
+ h ^= id.charCodeAt(i);
75
+ h = Math.imul(h, 16777619);
76
+ }
77
+ const hue = (h >>> 0) % 360;
78
+ return `hsl(${hue} 72% 42%)`;
79
+ }
80
+ function strokePaint(tool, fallback) {
81
+ switch (tool) {
82
+ case "laser":
83
+ return { stroke: "#f43f5e", strokeOpacity: 0.92, widthWorld: 4 };
84
+ case "marker":
85
+ return { stroke: fallback, strokeOpacity: 0.45, widthWorld: 14 };
86
+ case "brush":
87
+ return { stroke: fallback, strokeOpacity: 0.85, widthWorld: 5 };
88
+ case "pencil":
89
+ return { stroke: fallback, strokeOpacity: 0.9, widthWorld: 2.5 };
90
+ default:
91
+ return { stroke: fallback, strokeOpacity: 0.95, widthWorld: 3.5 };
92
+ }
93
+ }
94
+ function PresenceRemoteLayer({
95
+ camera,
96
+ cameraVersion: _cameraVersion,
97
+ peers
98
+ }) {
99
+ const z = camera.zoom;
100
+ const rootTransform = formatCameraTransform(camera);
101
+ const overlayStrokePx = 1.25;
102
+ const LUCIDE_POINTER_VIEWBOX = 24;
103
+ const remoteCursorScreenPx = 15;
104
+ const iconWorldScale = remoteCursorScreenPx / (LUCIDE_POINTER_VIEWBOX * z);
105
+ return /* @__PURE__ */ jsx(
106
+ "svg",
107
+ {
108
+ style: {
109
+ position: "absolute",
110
+ top: 0,
111
+ left: 0,
112
+ right: 0,
113
+ bottom: 0,
114
+ zIndex: 9,
115
+ width: "100%",
116
+ height: "100%",
117
+ touchAction: "none",
118
+ pointerEvents: "none"
119
+ },
120
+ role: "presentation",
121
+ "aria-hidden": true,
122
+ width: "100%",
123
+ height: "100%",
124
+ children: /* @__PURE__ */ jsx("g", { transform: rootTransform, children: peers.map((peer) => {
125
+ const color = peer.color ?? defaultPresenceColorForId(peer.id);
126
+ const markup = peer.markupStroke;
127
+ let strokeNode = null;
128
+ if (markup && markup.points.length > 0) {
129
+ const paint = strokePaint(markup.tool, color);
130
+ const d = markup.points.length >= 2 ? smoothFreehandPointsToPathD([...markup.points]) : null;
131
+ if (d) {
132
+ strokeNode = /* @__PURE__ */ jsx(
133
+ "path",
134
+ {
135
+ d,
136
+ fill: "none",
137
+ stroke: paint.stroke,
138
+ strokeOpacity: paint.strokeOpacity,
139
+ strokeWidth: Math.max(paint.widthWorld / z, overlayStrokePx),
140
+ strokeLinecap: "round",
141
+ strokeLinejoin: "round",
142
+ shapeRendering: "geometricPrecision",
143
+ vectorEffect: "non-scaling-stroke"
144
+ }
145
+ );
146
+ } else {
147
+ const p0 = markup.points[0];
148
+ if (p0) {
149
+ strokeNode = /* @__PURE__ */ jsx(
150
+ "circle",
151
+ {
152
+ cx: p0.x,
153
+ cy: p0.y,
154
+ r: Math.max(3 / z, 2),
155
+ fill: paint.stroke,
156
+ fillOpacity: paint.strokeOpacity,
157
+ vectorEffect: "non-scaling-stroke"
158
+ }
159
+ );
160
+ }
161
+ }
162
+ }
163
+ const cur = peer.cursor;
164
+ let cursorNode = null;
165
+ if (cur) {
166
+ const displayName = peer.displayName;
167
+ const labelOffsetX = 10 / z;
168
+ const labelOffsetY = 10 / z;
169
+ const labelFont = 10 / z;
170
+ cursorNode = /* @__PURE__ */ jsxs("g", { children: [
171
+ /* @__PURE__ */ jsx(
172
+ "g",
173
+ {
174
+ transform: `translate(${cur.x}, ${cur.y}) scale(${iconWorldScale}) translate(${ -4.037}, ${ -4.688})`,
175
+ children: /* @__PURE__ */ jsx(
176
+ MousePointer2,
177
+ {
178
+ size: LUCIDE_POINTER_VIEWBOX,
179
+ color,
180
+ fill: color,
181
+ stroke: "#ffffff",
182
+ strokeWidth: 1.25,
183
+ absoluteStrokeWidth: true,
184
+ "aria-hidden": true
185
+ }
186
+ )
187
+ }
188
+ ),
189
+ displayName ? /* @__PURE__ */ jsx(
190
+ "text",
191
+ {
192
+ x: cur.x + labelOffsetX,
193
+ y: cur.y + labelOffsetY,
194
+ fill: color,
195
+ stroke: "#ffffff",
196
+ strokeWidth: 2.5 / z,
197
+ paintOrder: "stroke",
198
+ style: {
199
+ fontSize: labelFont,
200
+ fontFamily: "system-ui, sans-serif",
201
+ fontWeight: 600
202
+ },
203
+ vectorEffect: "non-scaling-stroke",
204
+ children: displayName
205
+ }
206
+ ) : null
207
+ ] });
208
+ }
209
+ return /* @__PURE__ */ jsxs("g", { children: [
210
+ strokeNode,
211
+ cursorNode
212
+ ] }, peer.id);
213
+ }) })
214
+ }
215
+ );
216
+ }
217
+
218
+ // src/react/plugins/realtime/protocol.ts
219
+ function isRecord(value) {
220
+ return typeof value === "object" && value !== null && !Array.isArray(value);
221
+ }
222
+ function getString(value) {
223
+ return typeof value === "string" ? value : void 0;
224
+ }
225
+ function getNumber(value) {
226
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
227
+ }
228
+ function parseCursor(value) {
229
+ if (value === null) return null;
230
+ if (!isRecord(value)) return void 0;
231
+ const x = getNumber(value.x);
232
+ const y = getNumber(value.y);
233
+ if (x == null || y == null) return void 0;
234
+ return { x, y };
235
+ }
236
+ function parseMarkupStroke(value) {
237
+ if (value === null) return null;
238
+ if (!isRecord(value) || !Array.isArray(value.points)) return void 0;
239
+ const tool = getString(value.tool);
240
+ if (tool !== "draw" && tool !== "pencil" && tool !== "brush" && tool !== "marker" && tool !== "laser") {
241
+ return void 0;
242
+ }
243
+ const points = value.points.map((point) => {
244
+ if (!isRecord(point)) return null;
245
+ const x = getNumber(point.x);
246
+ const y = getNumber(point.y);
247
+ if (x == null || y == null) return null;
248
+ return { x, y };
249
+ }).filter((point) => point != null);
250
+ return { points, tool };
251
+ }
252
+ function parsePresencePayload(value) {
253
+ if (!isRecord(value)) return void 0;
254
+ const cursor = parseCursor(value.cursor);
255
+ if (cursor === void 0) return void 0;
256
+ const markupStroke = parseMarkupStroke(value.markupStroke);
257
+ if (markupStroke === void 0 && value.markupStroke !== void 0)
258
+ return void 0;
259
+ return {
260
+ cursor,
261
+ ...markupStroke !== void 0 ? { markupStroke } : {},
262
+ ...getString(value.activeTool) ? { activeTool: getString(value.activeTool) } : {}
263
+ };
264
+ }
265
+ function parseItems(value) {
266
+ return Array.isArray(value) ? value : void 0;
267
+ }
268
+ function parseDocumentSnapshot(value) {
269
+ if (!isRecord(value)) return void 0;
270
+ const revision = getNumber(value.revision);
271
+ const updatedAt = getNumber(value.updatedAt);
272
+ const items = parseItems(value.items);
273
+ if (revision == null || updatedAt == null || items == null) return void 0;
274
+ return {
275
+ revision,
276
+ updatedAt,
277
+ items,
278
+ ...getString(value.updatedByClientId) ? { updatedByClientId: getString(value.updatedByClientId) } : {}
279
+ };
280
+ }
281
+ function parseRealtimeSessionPeer(value) {
282
+ if (!isRecord(value)) return void 0;
283
+ const clientId = getString(value.clientId);
284
+ const peerId = getString(value.peerId);
285
+ const roomId = getString(value.roomId);
286
+ const joinedAt = getNumber(value.joinedAt);
287
+ const lastSeenAt = getNumber(value.lastSeenAt);
288
+ const cursor = parseCursor(value.cursor);
289
+ if (clientId == null || peerId == null || roomId == null || joinedAt == null || lastSeenAt == null || cursor === void 0) {
290
+ return void 0;
291
+ }
292
+ const markupStroke = parseMarkupStroke(value.markupStroke);
293
+ if (markupStroke === void 0 && value.markupStroke !== void 0)
294
+ return void 0;
295
+ const isSelf = value.isSelf === true;
296
+ const connectionState = getString(value.connectionState);
297
+ return {
298
+ id: clientId,
299
+ clientId,
300
+ peerId,
301
+ roomId,
302
+ joinedAt,
303
+ lastSeenAt,
304
+ isSelf,
305
+ cursor,
306
+ ...getString(value.displayName) ? { displayName: getString(value.displayName) } : {},
307
+ ...getString(value.color) ? { color: getString(value.color) } : {},
308
+ ...getString(value.image) ? { image: getString(value.image) } : {},
309
+ ...markupStroke !== void 0 ? { markupStroke } : {},
310
+ ...getString(value.activeTool) ? { activeTool: getString(value.activeTool) } : {},
311
+ ...connectionState ? { connectionState } : {}
312
+ };
313
+ }
314
+ function parsePeers(value) {
315
+ if (!Array.isArray(value)) return void 0;
316
+ const peers = value.map((peer) => parseRealtimeSessionPeer(peer)).filter((peer) => peer != null);
317
+ return peers.length === value.length ? peers : void 0;
318
+ }
319
+ function parseRealtimeClientMessage(value) {
320
+ if (!isRecord(value)) return void 0;
321
+ const type = getString(value.type);
322
+ if (type === "session:join") {
323
+ const roomId = getString(value.roomId);
324
+ const peer = isRecord(value.peer) ? value.peer : void 0;
325
+ const clientId = peer ? getString(peer.clientId) : void 0;
326
+ const peerId = peer ? getString(peer.peerId) : void 0;
327
+ if (!roomId || !peer || !clientId || !peerId) return void 0;
328
+ return {
329
+ type,
330
+ roomId,
331
+ peer: {
332
+ clientId,
333
+ peerId,
334
+ ...getString(peer.displayName) ? { displayName: getString(peer.displayName) } : {},
335
+ ...getString(peer.color) ? { color: getString(peer.color) } : {},
336
+ ...getString(peer.image) ? { image: getString(peer.image) } : {}
337
+ }
338
+ };
339
+ }
340
+ if (type === "session:leave" || type === "session:ping") {
341
+ const roomId = getString(value.roomId);
342
+ const clientId = getString(value.clientId);
343
+ if (!roomId || !clientId) return void 0;
344
+ if (type === "session:leave") {
345
+ return { type, roomId, clientId };
346
+ }
347
+ const sentAt = getNumber(value.sentAt);
348
+ if (sentAt == null) return void 0;
349
+ return { type, roomId, clientId, sentAt };
350
+ }
351
+ if (type === "presence:update") {
352
+ const roomId = getString(value.roomId);
353
+ const clientId = getString(value.clientId);
354
+ const presence = parsePresencePayload(value.presence);
355
+ if (!roomId || !clientId || !presence) return void 0;
356
+ return { type, roomId, clientId, presence };
357
+ }
358
+ if (type === "document:update") {
359
+ const roomId = getString(value.roomId);
360
+ const clientId = getString(value.clientId);
361
+ const baseRevision = getNumber(value.baseRevision);
362
+ const items = parseItems(value.items);
363
+ if (!roomId || !clientId || baseRevision == null || !items) return void 0;
364
+ return { type, roomId, clientId, baseRevision, items };
365
+ }
366
+ return void 0;
367
+ }
368
+ function parseRealtimeServerMessage(value) {
369
+ if (!isRecord(value)) return void 0;
370
+ const type = getString(value.type);
371
+ if (type === "session:welcome") {
372
+ const roomId = getString(value.roomId);
373
+ const clientId = getString(value.clientId);
374
+ const serverTime = getNumber(value.serverTime);
375
+ const document2 = parseDocumentSnapshot(value.document);
376
+ const peers = parsePeers(value.peers);
377
+ if (!roomId || !clientId || serverTime == null || !document2 || !peers) {
378
+ return void 0;
379
+ }
380
+ return { type, roomId, clientId, serverTime, document: document2, peers };
381
+ }
382
+ if (type === "session:peer-joined") {
383
+ const roomId = getString(value.roomId);
384
+ const peer = parseRealtimeSessionPeer(value.peer);
385
+ if (!roomId || !peer) return void 0;
386
+ return { type, roomId, peer };
387
+ }
388
+ if (type === "session:peer-left") {
389
+ const roomId = getString(value.roomId);
390
+ const clientId = getString(value.clientId);
391
+ if (!roomId || !clientId) return void 0;
392
+ return { type, roomId, clientId };
393
+ }
394
+ if (type === "session:pong") {
395
+ const roomId = getString(value.roomId);
396
+ const clientId = getString(value.clientId);
397
+ const sentAt = getNumber(value.sentAt);
398
+ const serverTime = getNumber(value.serverTime);
399
+ if (!roomId || !clientId || sentAt == null || serverTime == null) {
400
+ return void 0;
401
+ }
402
+ return { type, roomId, clientId, sentAt, serverTime };
403
+ }
404
+ if (type === "session:error") {
405
+ const code = getString(value.code);
406
+ const message = getString(value.message);
407
+ if (!code || !message) return void 0;
408
+ return {
409
+ type,
410
+ code,
411
+ message,
412
+ ...getString(value.roomId) ? { roomId: getString(value.roomId) } : {}
413
+ };
414
+ }
415
+ if (type === "presence:sync") {
416
+ const roomId = getString(value.roomId);
417
+ const peers = parsePeers(value.peers);
418
+ const serverTime = getNumber(value.serverTime);
419
+ if (!roomId || !peers || serverTime == null) return void 0;
420
+ return { type, roomId, peers, serverTime };
421
+ }
422
+ if (type === "document:sync" || type === "document:resync-required") {
423
+ const roomId = getString(value.roomId);
424
+ const document2 = parseDocumentSnapshot(value.document);
425
+ if (!roomId || !document2) return void 0;
426
+ if (type === "document:sync") {
427
+ return { type, roomId, document: document2 };
428
+ }
429
+ const reason = getString(value.reason);
430
+ if (!reason) return void 0;
431
+ return { type, roomId, reason, document: document2 };
432
+ }
433
+ return void 0;
434
+ }
435
+ var shell = {
436
+ position: "absolute",
437
+ top: 12,
438
+ left: "50%",
439
+ transform: "translateX(-50%)",
440
+ display: "flex",
441
+ flexDirection: "row",
442
+ alignItems: "center",
443
+ flexWrap: "wrap",
444
+ justifyContent: "center",
445
+ gap: "0 14px",
446
+ maxWidth: "min(100% - 24px, 520px)",
447
+ padding: "6px 14px",
448
+ borderRadius: 9999,
449
+ border: "1px solid rgba(15, 23, 42, 0.08)",
450
+ background: "rgba(255, 255, 255, 0.78)",
451
+ backdropFilter: "blur(12px)",
452
+ WebkitBackdropFilter: "blur(12px)",
453
+ boxShadow: "0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 16px rgba(15, 23, 42, 0.06)",
454
+ pointerEvents: "auto",
455
+ userSelect: "none",
456
+ WebkitUserSelect: "none",
457
+ WebkitTouchCallout: "none",
458
+ fontSize: "0.6875rem",
459
+ fontWeight: 500,
460
+ letterSpacing: "0.01em",
461
+ color: "#334155"
462
+ };
463
+ var titleStyle = {
464
+ fontWeight: 600,
465
+ color: "#64748b",
466
+ letterSpacing: "0.06em",
467
+ textTransform: "uppercase",
468
+ fontSize: "0.625rem",
469
+ whiteSpace: "nowrap"
470
+ };
471
+ var statusRow = {
472
+ display: "inline-flex",
473
+ alignItems: "center",
474
+ gap: 6,
475
+ flexShrink: 0
476
+ };
477
+ var peerList = {
478
+ display: "flex",
479
+ alignItems: "center",
480
+ flexWrap: "wrap",
481
+ gap: "4px 10px",
482
+ margin: 0,
483
+ padding: 0,
484
+ listStyle: "none",
485
+ justifyContent: "center"
486
+ };
487
+ var peerItem = {
488
+ display: "inline-flex",
489
+ alignItems: "center",
490
+ gap: 5,
491
+ maxWidth: 140
492
+ };
493
+ var dot = {
494
+ width: 6,
495
+ height: 6,
496
+ borderRadius: 999,
497
+ flexShrink: 0
498
+ };
499
+ var avatarStyle = {
500
+ width: 18,
501
+ height: 18,
502
+ borderRadius: 999,
503
+ objectFit: "cover",
504
+ flexShrink: 0,
505
+ border: "1px solid rgba(15, 23, 42, 0.08)"
506
+ };
507
+ var nameStyle = {
508
+ overflow: "hidden",
509
+ textOverflow: "ellipsis",
510
+ whiteSpace: "nowrap"
511
+ };
512
+ function connectionAppearance(connectionState) {
513
+ if (connectionState === "connected") {
514
+ return {
515
+ color: "#16a34a",
516
+ label: "Live",
517
+ shadow: "0 0 0 2px rgba(22, 163, 74, 0.25)"
518
+ };
519
+ }
520
+ if (connectionState === "connecting") {
521
+ return {
522
+ color: "#2563eb",
523
+ label: "Conectando",
524
+ shadow: "0 0 0 2px rgba(37, 99, 235, 0.18)"
525
+ };
526
+ }
527
+ if (connectionState === "reconnecting") {
528
+ return {
529
+ color: "#d97706",
530
+ label: "Reconectando",
531
+ shadow: "0 0 0 2px rgba(217, 119, 6, 0.2)"
532
+ };
533
+ }
534
+ if (connectionState === "error") {
535
+ return {
536
+ color: "#dc2626",
537
+ label: "Erro",
538
+ shadow: "none"
539
+ };
540
+ }
541
+ return {
542
+ color: "#64748b",
543
+ label: "Offline",
544
+ shadow: "none"
545
+ };
546
+ }
547
+ function RealtimeSessionPanel({
548
+ title = "Session",
549
+ peers,
550
+ connected = true,
551
+ connectionState,
552
+ roomId,
553
+ children
554
+ }) {
555
+ const resolvedConnectionState = connectionState ?? (connected ? "connected" : "reconnecting");
556
+ const status = connectionAppearance(resolvedConnectionState);
557
+ return /* @__PURE__ */ jsxs("div", { style: shell, "data-slot": "realtime-session-panel", children: [
558
+ /* @__PURE__ */ jsx("span", { style: titleStyle, children: title }),
559
+ roomId ? /* @__PURE__ */ jsxs(Fragment, { children: [
560
+ /* @__PURE__ */ jsx(
561
+ "span",
562
+ {
563
+ style: {
564
+ width: 1,
565
+ height: 14,
566
+ background: "rgba(15, 23, 42, 0.12)",
567
+ flexShrink: 0
568
+ },
569
+ "aria-hidden": true
570
+ }
571
+ ),
572
+ /* @__PURE__ */ jsxs("span", { style: { ...titleStyle, textTransform: "none", letterSpacing: 0 }, children: [
573
+ "Sala ",
574
+ roomId
575
+ ] })
576
+ ] }) : null,
577
+ /* @__PURE__ */ jsx(
578
+ "span",
579
+ {
580
+ style: {
581
+ width: 1,
582
+ height: 14,
583
+ background: "rgba(15, 23, 42, 0.12)",
584
+ flexShrink: 0
585
+ },
586
+ "aria-hidden": true
587
+ }
588
+ ),
589
+ /* @__PURE__ */ jsxs("span", { style: statusRow, children: [
590
+ /* @__PURE__ */ jsx(
591
+ "span",
592
+ {
593
+ style: {
594
+ ...dot,
595
+ background: status.color,
596
+ boxShadow: status.shadow
597
+ },
598
+ "aria-hidden": true
599
+ }
600
+ ),
601
+ /* @__PURE__ */ jsx("span", { style: { color: status.color, fontWeight: 600 }, children: status.label })
602
+ ] }),
603
+ peers.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
604
+ /* @__PURE__ */ jsx(
605
+ "span",
606
+ {
607
+ style: {
608
+ width: 1,
609
+ height: 14,
610
+ background: "rgba(15, 23, 42, 0.12)",
611
+ flexShrink: 0
612
+ },
613
+ "aria-hidden": true
614
+ }
615
+ ),
616
+ /* @__PURE__ */ jsx("ul", { style: peerList, children: peers.map((p) => /* @__PURE__ */ jsxs("li", { style: peerItem, children: [
617
+ "image" in p && p.image ? /* @__PURE__ */ jsx("img", { src: p.image, alt: "", "aria-hidden": true, style: avatarStyle }) : /* @__PURE__ */ jsx(
618
+ "span",
619
+ {
620
+ "aria-hidden": true,
621
+ style: {
622
+ ...dot,
623
+ background: p.color ?? "#94a3b8"
624
+ }
625
+ }
626
+ ),
627
+ /* @__PURE__ */ jsxs("span", { style: nameStyle, children: [
628
+ p.displayName ?? p.id,
629
+ "isSelf" in p && p.isSelf ? " (voc\xEA)" : ""
630
+ ] })
631
+ ] }, p.id)) })
632
+ ] }) : null,
633
+ children
634
+ ] });
635
+ }
636
+ var CanvuPluginContext = createContext(
637
+ null
638
+ );
639
+ function createCanvuPlugin(plugin) {
640
+ return plugin;
641
+ }
642
+ function useCanvuPluginContext() {
643
+ const ctx = useContext(CanvuPluginContext);
644
+ if (!ctx) {
645
+ throw new Error(
646
+ "useCanvuPluginContext must be used inside a VectorViewport plugin runtime."
647
+ );
648
+ }
649
+ return ctx;
650
+ }
651
+ function useCanvuViewportContext() {
652
+ const { viewportRef, viewport } = useCanvuPluginContext();
653
+ return { viewportRef, viewport };
654
+ }
655
+ function useCanvuDocumentContext() {
656
+ const { viewport } = useCanvuPluginContext();
657
+ return {
658
+ items: viewport.items,
659
+ onItemsChange: viewport.onItemsChange
660
+ };
661
+ }
662
+ function useCanvuPluginContribution(pluginId, contribution) {
663
+ const { registerContribution, unregisterContribution } = useCanvuPluginContext();
664
+ useLayoutEffect(() => {
665
+ registerContribution(pluginId, contribution);
666
+ return () => unregisterContribution(pluginId);
667
+ }, [contribution, pluginId, registerContribution, unregisterContribution]);
668
+ }
669
+ var base = {
670
+ width: 20,
671
+ height: 20,
672
+ viewBox: "0 0 24 24",
673
+ fill: "none",
674
+ stroke: "currentColor",
675
+ strokeWidth: 2,
676
+ strokeLinecap: "round",
677
+ strokeLinejoin: "round"
678
+ };
679
+ function IconLaser(props) {
680
+ return /* @__PURE__ */ jsxs("svg", { ...base, ...props, "aria-hidden": true, children: [
681
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.5" }),
682
+ /* @__PURE__ */ jsx("path", { d: "M12 4v4" }),
683
+ /* @__PURE__ */ jsx("path", { d: "M12 16v4" }),
684
+ /* @__PURE__ */ jsx("path", { d: "M4 12h4" }),
685
+ /* @__PURE__ */ jsx("path", { d: "M16 12h4" })
686
+ ] });
687
+ }
688
+ var ic = { size: 20, strokeWidth: 2 };
689
+ var DEFAULT_VECTOR_TOOLS = [
690
+ {
691
+ id: "hand",
692
+ label: "Hand",
693
+ icon: /* @__PURE__ */ jsx(Hand, { ...ic, "aria-hidden": true }),
694
+ shortcutHint: "H"
695
+ },
696
+ {
697
+ id: "select",
698
+ label: "Select",
699
+ icon: /* @__PURE__ */ jsx(MousePointer2, { ...ic, "aria-hidden": true }),
700
+ shortcutHint: "V"
701
+ },
702
+ {
703
+ id: "rect",
704
+ label: "Rectangle",
705
+ icon: /* @__PURE__ */ jsx(Square, { ...ic, "aria-hidden": true }),
706
+ shortcutHint: "R"
707
+ },
708
+ {
709
+ id: "ellipse",
710
+ label: "Ellipse",
711
+ icon: /* @__PURE__ */ jsx(Circle, { ...ic, "aria-hidden": true }),
712
+ shortcutHint: "O"
713
+ },
714
+ {
715
+ id: "line",
716
+ label: "Line",
717
+ icon: /* @__PURE__ */ jsx(Minus, { ...ic, "aria-hidden": true }),
718
+ shortcutHint: "L"
719
+ },
720
+ {
721
+ id: "arrow",
722
+ label: "Arrow",
723
+ icon: /* @__PURE__ */ jsx(ArrowUpRight, { ...ic, "aria-hidden": true }),
724
+ shortcutHint: "A"
725
+ },
726
+ {
727
+ id: "draw",
728
+ label: "Desenhar",
729
+ tooltipLabel: "Draw",
730
+ icon: /* @__PURE__ */ jsx(PenLine, { ...ic, "aria-hidden": true }),
731
+ shortcutHint: "D"
732
+ },
733
+ {
734
+ id: "marker",
735
+ label: "Realce",
736
+ tooltipLabel: "Highlighter",
737
+ icon: /* @__PURE__ */ jsx(Highlighter, { ...ic, "aria-hidden": true }),
738
+ shortcutHint: "M"
739
+ },
740
+ {
741
+ id: "laser",
742
+ label: "Laser",
743
+ icon: /* @__PURE__ */ jsx(IconLaser, { "aria-hidden": true }),
744
+ shortcutHint: "K"
745
+ },
746
+ {
747
+ id: "eraser",
748
+ label: "Borracha",
749
+ tooltipLabel: "Eraser",
750
+ icon: /* @__PURE__ */ jsx(Eraser, { ...ic, "aria-hidden": true }),
751
+ shortcutHint: "E"
752
+ },
753
+ {
754
+ id: "text",
755
+ label: "Text",
756
+ icon: /* @__PURE__ */ jsx(Type, { ...ic, "aria-hidden": true }),
757
+ shortcutHint: "T"
758
+ },
759
+ {
760
+ id: "image",
761
+ label: "File",
762
+ icon: /* @__PURE__ */ jsx(Image, { ...ic, "aria-hidden": true }),
763
+ shortcutHint: "I"
764
+ }
765
+ ];
766
+
767
+ // src/scene/custom-shape.ts
768
+ function expandCustomShapeTemplate(template, width, height) {
769
+ return template.replace(/\{\{w\}\}/g, String(width)).replace(/\{\{h\}\}/g, String(height)).replace(/\{\{width\}\}/g, String(width)).replace(/\{\{height\}\}/g, String(height));
770
+ }
771
+ function resolveCustomInner(content, size) {
772
+ if ("render" in content) {
773
+ return content.render(size);
774
+ }
775
+ return expandCustomShapeTemplate(content.svg, size.width, size.height);
776
+ }
777
+ function buildCustomShapeChildrenSvg(inner, intrinsic, bounds) {
778
+ const b = normalizeRect(bounds);
779
+ const sx = b.width / intrinsic.width;
780
+ const sy = b.height / intrinsic.height;
781
+ return `<g transform="scale(${sx},${sy})">${inner}</g>`;
782
+ }
783
+ function createCustomShapeItem(id, bounds, content) {
784
+ const r = normalizeRect(bounds);
785
+ const intrinsic = { width: r.width, height: r.height };
786
+ const inner = resolveCustomInner(content, intrinsic);
787
+ return {
788
+ id,
789
+ x: r.x,
790
+ y: r.y,
791
+ bounds: { ...r },
792
+ toolKind: "custom",
793
+ customIntrinsicSize: intrinsic,
794
+ customInnerSvg: inner,
795
+ childrenSvg: buildCustomShapeChildrenSvg(inner, intrinsic, r)
796
+ };
797
+ }
798
+ var iconProps = { size: 20, strokeWidth: 2 };
799
+ var COMMENT_PLUGIN_DATA_KEY = "realtimeComment";
800
+ var REALTIME_COMMENT_TOOL = {
801
+ id: "comment",
802
+ label: "Coment\xE1rio",
803
+ tooltipLabel: "Comment",
804
+ ariaLabel: "Adicionar coment\xE1rio colaborativo",
805
+ shortcutHint: "C",
806
+ icon: /* @__PURE__ */ jsx(MessageSquare, { ...iconProps, "aria-hidden": true })
807
+ };
808
+ function isRecord2(value) {
809
+ return typeof value === "object" && value !== null && !Array.isArray(value);
810
+ }
811
+ function getString2(value) {
812
+ return typeof value === "string" ? value : void 0;
813
+ }
814
+ function getNumber2(value) {
815
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
816
+ }
817
+ function initialsFromDisplayName(displayName) {
818
+ const words = displayName.trim().split(/\s+/).filter(Boolean).slice(0, 2);
819
+ if (words.length === 0) return "?";
820
+ return words.map((part) => part[0]?.toUpperCase() ?? "").join("") || "?";
821
+ }
822
+ function escapeSvgText(value) {
823
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
824
+ }
825
+ function createRealtimeCommentAvatarDataUrl(displayName, color) {
826
+ const initials = initialsFromDisplayName(displayName);
827
+ const svg = `
828
+ <svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
829
+ <defs>
830
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
831
+ <stop offset="0%" stop-color="${color}" />
832
+ <stop offset="100%" stop-color="#0f172a" />
833
+ </linearGradient>
834
+ <radialGradient id="shine" cx="30%" cy="24%" r="70%">
835
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.48" />
836
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0" />
837
+ </radialGradient>
838
+ </defs>
839
+ <rect width="96" height="96" rx="28" fill="url(#bg)" />
840
+ <rect width="96" height="96" rx="28" fill="url(#shine)" />
841
+ <circle cx="48" cy="38" r="18" fill="rgba(255,255,255,0.2)" />
842
+ <path d="M20 84c5-16 17-24 28-24s23 8 28 24" fill="rgba(255,255,255,0.16)" />
843
+ <text x="48" y="56" text-anchor="middle" font-size="28" font-weight="700" font-family="system-ui, sans-serif" fill="#ffffff">${escapeSvgText(initials)}</text>
844
+ </svg>
845
+ `;
846
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
847
+ }
848
+ function commentPlaceholderInnerSvg(color) {
849
+ return `
850
+ <circle cx="9" cy="9" r="7" fill="rgba(255,255,255,0.94)" stroke="${color}" stroke-width="1.2" stroke-dasharray="2 1.5" />
851
+ <circle cx="9" cy="9" r="3.1" fill="${color}" fill-opacity="0.18" stroke="${color}" stroke-width="1" />
852
+ <path d="M7.1 8.1h3.8c.8 0 1.4.5 1.4 1.2v1.1c0 .7-.6 1.2-1.4 1.2H9.3l-1.2 1v-1H7.1c-.8 0-1.4-.5-1.4-1.2V9.3c0-.7.6-1.2 1.4-1.2Z" fill="#ffffff" stroke="${color}" stroke-width="0.9" />
853
+ `;
854
+ }
855
+ function commentAnchorInnerSvg(authorColor) {
856
+ return `
857
+ <circle cx="9" cy="9" r="7.2" fill="rgba(255,255,255,0.96)" stroke="rgba(15,23,42,0.1)" stroke-width="0.9" />
858
+ <circle cx="9" cy="9" r="4.1" fill="${authorColor}" />
859
+ <circle cx="13.2" cy="13.2" r="3.2" fill="#ffffff" stroke="rgba(15,23,42,0.12)" stroke-width="0.9" />
860
+ <path d="M11.7 12.1h2.7c.6 0 1 .4 1 .9v.9c0 .5-.4.9-1 .9h-1.1l-1 .8v-.8h-.4c-.6 0-1-.4-1-.9V13c0-.5.4-.9 1-.9Z" fill="#111827" fill-opacity="0.08" stroke="#111827" stroke-width="0.75" />
861
+ `;
862
+ }
863
+ function createRealtimeCommentDraftItem(id, bounds, seedColor = "#7c3aed") {
864
+ const base2 = createCustomShapeItem(id, bounds, {
865
+ render: () => commentPlaceholderInnerSvg(seedColor)
866
+ });
867
+ return {
868
+ ...base2,
869
+ pluginData: {
870
+ ...base2.pluginData ?? {},
871
+ [COMMENT_PLUGIN_DATA_KEY]: {
872
+ kind: "draft",
873
+ seedColor
874
+ }
875
+ }
876
+ };
877
+ }
878
+ function createRealtimeCommentItem(id, bounds, comment) {
879
+ const base2 = createCustomShapeItem(id, bounds, {
880
+ render: () => commentAnchorInnerSvg(comment.authorColor)
881
+ });
882
+ return {
883
+ ...base2,
884
+ pluginData: {
885
+ ...base2.pluginData ?? {},
886
+ [COMMENT_PLUGIN_DATA_KEY]: {
887
+ kind: "comment",
888
+ ...comment
889
+ }
890
+ }
891
+ };
892
+ }
893
+ function getStoredRealtimeCommentData(item) {
894
+ const pluginData = item.pluginData?.[COMMENT_PLUGIN_DATA_KEY];
895
+ if (!isRecord2(pluginData)) return null;
896
+ const kind = getString2(pluginData.kind);
897
+ if (kind === "draft") {
898
+ return {
899
+ kind,
900
+ ...getString2(pluginData.seedColor) ? { seedColor: getString2(pluginData.seedColor) } : {}
901
+ };
902
+ }
903
+ if (kind !== "comment") return null;
904
+ const body = getString2(pluginData.body);
905
+ const createdAt = getNumber2(pluginData.createdAt);
906
+ const authorPeerId = getString2(pluginData.authorPeerId);
907
+ const authorDisplayName = getString2(pluginData.authorDisplayName);
908
+ const authorColor = getString2(pluginData.authorColor);
909
+ const authorImage = getString2(pluginData.authorImage);
910
+ if (!body || createdAt == null || !authorPeerId || !authorDisplayName || !authorColor || !authorImage) {
911
+ return null;
912
+ }
913
+ return {
914
+ kind,
915
+ body,
916
+ createdAt,
917
+ authorPeerId,
918
+ authorDisplayName,
919
+ authorColor,
920
+ authorImage
921
+ };
922
+ }
923
+ function isRealtimeCommentDraftItem(item) {
924
+ return getStoredRealtimeCommentData(item)?.kind === "draft";
925
+ }
926
+ function isRealtimeCommentItem(item) {
927
+ return getStoredRealtimeCommentData(item)?.kind === "comment";
928
+ }
929
+ function getRealtimeCommentData(item) {
930
+ const data = getStoredRealtimeCommentData(item);
931
+ return data?.kind === "comment" ? data : null;
932
+ }
933
+ function withRealtimeCommentTool(tools) {
934
+ if (tools.some((tool) => tool.id === REALTIME_COMMENT_TOOL.id)) {
935
+ return tools.map(
936
+ (tool) => tool.id === REALTIME_COMMENT_TOOL.id ? { ...tool, ...REALTIME_COMMENT_TOOL } : tool
937
+ );
938
+ }
939
+ const textIndex = tools.findIndex((tool) => tool.id === "text");
940
+ if (textIndex < 0) return [...tools, REALTIME_COMMENT_TOOL];
941
+ return [
942
+ ...tools.slice(0, textIndex + 1),
943
+ REALTIME_COMMENT_TOOL,
944
+ ...tools.slice(textIndex + 1)
945
+ ];
946
+ }
947
+ var overlayShell = {
948
+ position: "absolute",
949
+ inset: 0,
950
+ pointerEvents: "none",
951
+ zIndex: 26,
952
+ overflow: "hidden"
953
+ };
954
+ function formatCommentTimestamp(timestamp) {
955
+ return new Intl.DateTimeFormat("pt-BR", {
956
+ dateStyle: "short",
957
+ timeStyle: "short"
958
+ }).format(new Date(timestamp));
959
+ }
960
+ function RealtimeCommentsOverlay({
961
+ items,
962
+ onItemsChange,
963
+ isDragEnabled = false,
964
+ cameraVersion,
965
+ viewportRef
966
+ }) {
967
+ const [hoveredId, setHoveredId] = useState(null);
968
+ const [draggedId, setDraggedId] = useState(null);
969
+ const dragStateRef = useRef(null);
970
+ const itemsRef = useRef(items);
971
+ useEffect(() => {
972
+ itemsRef.current = items;
973
+ }, [items]);
974
+ const camera = viewportRef?.current?.getCamera() ?? null;
975
+ if (!camera) return null;
976
+ function moveCommentItem(item, deltaX, deltaY) {
977
+ return {
978
+ ...item,
979
+ x: item.x + deltaX,
980
+ y: item.y + deltaY,
981
+ bounds: {
982
+ ...item.bounds,
983
+ x: item.bounds.x + deltaX,
984
+ y: item.bounds.y + deltaY
985
+ }
986
+ };
987
+ }
988
+ function handleCommentPointerDown(itemId, event) {
989
+ if (event.button !== 0 || !onItemsChange || !isDragEnabled) return;
990
+ event.preventDefault();
991
+ event.stopPropagation();
992
+ const currentCamera = viewportRef?.current?.getCamera();
993
+ if (!currentCamera) return;
994
+ const start = currentCamera.screenToWorld(event.clientX, event.clientY);
995
+ dragStateRef.current = {
996
+ itemId,
997
+ lastWorldX: start.worldX,
998
+ lastWorldY: start.worldY
999
+ };
1000
+ setDraggedId(itemId);
1001
+ setHoveredId((current) => current === itemId ? null : current);
1002
+ const onPointerMove = (moveEvent) => {
1003
+ const drag = dragStateRef.current;
1004
+ const liveCamera = viewportRef?.current?.getCamera();
1005
+ if (!drag || !liveCamera) return;
1006
+ const next = liveCamera.screenToWorld(moveEvent.clientX, moveEvent.clientY);
1007
+ const deltaX = next.worldX - drag.lastWorldX;
1008
+ const deltaY = next.worldY - drag.lastWorldY;
1009
+ if (deltaX === 0 && deltaY === 0) return;
1010
+ drag.lastWorldX = next.worldX;
1011
+ drag.lastWorldY = next.worldY;
1012
+ const nextItems = itemsRef.current.map(
1013
+ (item) => item.id === drag.itemId ? moveCommentItem(item, deltaX, deltaY) : item
1014
+ );
1015
+ itemsRef.current = nextItems;
1016
+ onItemsChange(nextItems);
1017
+ };
1018
+ const onPointerUp = () => {
1019
+ dragStateRef.current = null;
1020
+ setDraggedId((current) => current === itemId ? null : current);
1021
+ window.removeEventListener("pointermove", onPointerMove);
1022
+ window.removeEventListener("pointerup", onPointerUp);
1023
+ window.removeEventListener("pointercancel", onPointerUp);
1024
+ };
1025
+ window.addEventListener("pointermove", onPointerMove);
1026
+ window.addEventListener("pointerup", onPointerUp);
1027
+ window.addEventListener("pointercancel", onPointerUp);
1028
+ }
1029
+ const comments = items.map((item) => ({ item, data: getRealtimeCommentData(item) })).filter(
1030
+ (entry) => entry.data != null
1031
+ );
1032
+ if (comments.length === 0) return null;
1033
+ return /* @__PURE__ */ jsx("div", { style: overlayShell, "data-slot": "realtime-comments-overlay", children: comments.map(({ item, data }) => {
1034
+ const anchor = camera.worldToScreen(
1035
+ item.bounds.x + item.bounds.width / 2,
1036
+ item.bounds.y + item.bounds.height / 2
1037
+ );
1038
+ const dragging = draggedId === item.id;
1039
+ const expanded = hoveredId === item.id && !dragging;
1040
+ return /* @__PURE__ */ jsx(
1041
+ "button",
1042
+ {
1043
+ type: "button",
1044
+ "aria-label": `Comment by ${data.authorDisplayName}`,
1045
+ onMouseEnter: () => setHoveredId(item.id),
1046
+ onMouseLeave: () => setHoveredId((current) => current === item.id ? null : current),
1047
+ onPointerDown: (event) => handleCommentPointerDown(item.id, event),
1048
+ style: {
1049
+ background: "transparent",
1050
+ border: 0,
1051
+ padding: 0,
1052
+ position: "absolute",
1053
+ left: anchor.screenX,
1054
+ top: anchor.screenY,
1055
+ transform: "translate(-50%, -50%)",
1056
+ pointerEvents: "auto",
1057
+ cursor: onItemsChange && isDragEnabled ? dragging ? "grabbing" : "grab" : "default"
1058
+ },
1059
+ children: /* @__PURE__ */ jsxs(
1060
+ "div",
1061
+ {
1062
+ style: {
1063
+ display: "flex",
1064
+ alignItems: expanded ? "flex-start" : "center",
1065
+ gap: expanded ? 9 : 0,
1066
+ width: expanded ? 228 : 44,
1067
+ minHeight: 44,
1068
+ padding: expanded ? "8px 10px 8px 8px" : 3,
1069
+ borderRadius: expanded ? 18 : 999,
1070
+ background: expanded ? "rgba(255,255,255,0.97)" : "transparent",
1071
+ border: expanded ? "1px solid rgba(148,163,184,0.28)" : "1px solid transparent",
1072
+ boxShadow: expanded ? "0 14px 32px rgba(15,23,42,0.18)" : "none",
1073
+ backdropFilter: "blur(10px)",
1074
+ transition: "width 160ms ease, border-radius 160ms ease, transform 160ms ease, box-shadow 160ms ease, padding 160ms ease",
1075
+ transform: dragging ? "scale(0.98)" : expanded ? "translateY(-2px)" : "translateY(0)",
1076
+ overflow: "hidden"
1077
+ },
1078
+ children: [
1079
+ /* @__PURE__ */ jsxs(
1080
+ "div",
1081
+ {
1082
+ style: {
1083
+ position: "relative",
1084
+ width: 36,
1085
+ height: 36,
1086
+ borderRadius: 999,
1087
+ flexShrink: 0,
1088
+ boxShadow: expanded ? `0 0 0 2px ${data.authorColor}22` : "0 10px 18px rgba(15,23,42,0.14)",
1089
+ border: expanded ? "none" : "2px solid rgba(255,255,255,0.92)",
1090
+ overflow: "hidden"
1091
+ },
1092
+ children: [
1093
+ /* @__PURE__ */ jsx(
1094
+ "img",
1095
+ {
1096
+ src: data.authorImage,
1097
+ alt: data.authorDisplayName,
1098
+ style: { width: "100%", height: "100%", objectFit: "cover" }
1099
+ }
1100
+ ),
1101
+ /* @__PURE__ */ jsx(
1102
+ "div",
1103
+ {
1104
+ style: {
1105
+ position: "absolute",
1106
+ right: expanded ? -1 : -2,
1107
+ bottom: expanded ? -1 : -2,
1108
+ width: 14,
1109
+ height: 14,
1110
+ borderRadius: 999,
1111
+ background: "#111827",
1112
+ color: "#fff",
1113
+ display: "grid",
1114
+ placeItems: "center",
1115
+ border: "1.5px solid rgba(255,255,255,0.98)",
1116
+ boxShadow: "0 4px 10px rgba(15,23,42,0.16)"
1117
+ },
1118
+ children: /* @__PURE__ */ jsx(Sparkles, { size: 8, strokeWidth: 2.2, "aria-hidden": true })
1119
+ }
1120
+ )
1121
+ ]
1122
+ }
1123
+ ),
1124
+ expanded ? /* @__PURE__ */ jsxs(
1125
+ "div",
1126
+ {
1127
+ style: {
1128
+ display: "grid",
1129
+ gap: 3,
1130
+ minWidth: 0,
1131
+ paddingTop: 1
1132
+ },
1133
+ children: [
1134
+ /* @__PURE__ */ jsxs(
1135
+ "div",
1136
+ {
1137
+ style: {
1138
+ display: "flex",
1139
+ justifyContent: "space-between",
1140
+ gap: 8,
1141
+ alignItems: "baseline"
1142
+ },
1143
+ children: [
1144
+ /* @__PURE__ */ jsx(
1145
+ "div",
1146
+ {
1147
+ style: {
1148
+ fontSize: 12,
1149
+ fontWeight: 700,
1150
+ color: "#0f172a",
1151
+ overflow: "hidden",
1152
+ textOverflow: "ellipsis",
1153
+ whiteSpace: "nowrap"
1154
+ },
1155
+ children: data.authorDisplayName
1156
+ }
1157
+ ),
1158
+ /* @__PURE__ */ jsx(
1159
+ "div",
1160
+ {
1161
+ style: {
1162
+ fontSize: 10,
1163
+ color: "#64748b",
1164
+ whiteSpace: "nowrap"
1165
+ },
1166
+ children: formatCommentTimestamp(data.createdAt)
1167
+ }
1168
+ )
1169
+ ]
1170
+ }
1171
+ ),
1172
+ /* @__PURE__ */ jsx(
1173
+ "div",
1174
+ {
1175
+ style: {
1176
+ fontSize: 11,
1177
+ lineHeight: 1.4,
1178
+ color: "#334155",
1179
+ display: "-webkit-box",
1180
+ WebkitLineClamp: 3,
1181
+ WebkitBoxOrient: "vertical",
1182
+ overflow: "hidden"
1183
+ },
1184
+ children: data.body
1185
+ }
1186
+ )
1187
+ ]
1188
+ }
1189
+ ) : null
1190
+ ]
1191
+ }
1192
+ )
1193
+ },
1194
+ item.id
1195
+ );
1196
+ }) });
1197
+ }
1198
+ function realtimeCommentsPlugin(options) {
1199
+ return {
1200
+ id: "trazo.plugin.realtime-comments",
1201
+ render: (ctx) => /* @__PURE__ */ jsx(RealtimeCommentsOverlay, { ...options, viewportRef: ctx.viewportRef })
1202
+ };
1203
+ }
1204
+ var COMMENT_BUBBLE_WORLD_SIZE = 18;
1205
+ function clamp(value, min, max) {
1206
+ return Math.min(max, Math.max(min, value));
1207
+ }
1208
+ function createCommentId() {
1209
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1210
+ return crypto.randomUUID();
1211
+ }
1212
+ return `comment-${Math.random().toString(36).slice(2, 10)}`;
1213
+ }
1214
+ function RealtimeCommentComposer({
1215
+ viewportRef,
1216
+ cameraVersion,
1217
+ composer,
1218
+ author,
1219
+ authorImage,
1220
+ roomId,
1221
+ title,
1222
+ description,
1223
+ placeholder,
1224
+ cancelLabel,
1225
+ submitLabel,
1226
+ submitHint,
1227
+ onBodyChange,
1228
+ onCommit,
1229
+ onCancel
1230
+ }) {
1231
+ const commentInputRef = useRef(null);
1232
+ const composerRef = useRef(null);
1233
+ const [composerSize, setComposerSize] = useState({ width: 360, height: 320 });
1234
+ useEffect(() => {
1235
+ if (!composer) return;
1236
+ commentInputRef.current?.focus();
1237
+ }, [composer]);
1238
+ const camera = viewportRef?.current?.getCamera();
1239
+ const viewportSize = viewportRef?.current?.getViewportSize();
1240
+ useLayoutEffect(() => {
1241
+ if (!composer || !viewportSize) return;
1242
+ const el = composerRef.current;
1243
+ if (!el) return;
1244
+ const updateSize = () => {
1245
+ const next = {
1246
+ width: el.offsetWidth,
1247
+ height: el.offsetHeight
1248
+ };
1249
+ setComposerSize(
1250
+ (current) => current.width === next.width && current.height === next.height ? current : next
1251
+ );
1252
+ };
1253
+ updateSize();
1254
+ if (typeof ResizeObserver === "undefined") {
1255
+ return;
1256
+ }
1257
+ const observer = new ResizeObserver(updateSize);
1258
+ observer.observe(el);
1259
+ return () => observer.disconnect();
1260
+ }, [composer, viewportSize]);
1261
+ if (!composer) return null;
1262
+ if (!camera || !viewportSize) return null;
1263
+ const anchor = camera.worldToScreen(composer.worldX, composer.worldY);
1264
+ const composerWidth = Math.min(360, Math.max(220, viewportSize.width - 32));
1265
+ const screenPosition = {
1266
+ left: clamp(
1267
+ anchor.screenX + 20,
1268
+ 16,
1269
+ Math.max(16, viewportSize.width - composerSize.width - 16)
1270
+ ),
1271
+ top: clamp(
1272
+ anchor.screenY - 22,
1273
+ 16,
1274
+ Math.max(16, viewportSize.height - composerSize.height - 16)
1275
+ )
1276
+ };
1277
+ function handleKeyDown(event) {
1278
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1279
+ event.preventDefault();
1280
+ onCommit();
1281
+ return;
1282
+ }
1283
+ if (event.key === "Escape") {
1284
+ event.preventDefault();
1285
+ onCancel();
1286
+ }
1287
+ }
1288
+ return /* @__PURE__ */ jsxs(
1289
+ "div",
1290
+ {
1291
+ ref: composerRef,
1292
+ style: {
1293
+ position: "absolute",
1294
+ left: screenPosition.left,
1295
+ top: screenPosition.top,
1296
+ zIndex: 44,
1297
+ width: composerWidth,
1298
+ maxWidth: "calc(100% - 32px)",
1299
+ maxHeight: "calc(100% - 32px)",
1300
+ padding: 18,
1301
+ borderRadius: 22,
1302
+ background: "rgba(255,255,255,0.98)",
1303
+ border: "1px solid rgba(148,163,184,0.28)",
1304
+ boxShadow: "0 22px 48px rgba(15,23,42,0.18)",
1305
+ backdropFilter: "blur(12px)",
1306
+ pointerEvents: "auto",
1307
+ boxSizing: "border-box",
1308
+ overflow: "auto"
1309
+ },
1310
+ children: [
1311
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 16, fontWeight: 700, color: "#0f172a" }, children: title }),
1312
+ /* @__PURE__ */ jsx("div", { style: { marginTop: 6, color: "#64748b", fontSize: 13, lineHeight: 1.5 }, children: description }),
1313
+ /* @__PURE__ */ jsxs(
1314
+ "div",
1315
+ {
1316
+ style: {
1317
+ display: "flex",
1318
+ alignItems: "center",
1319
+ gap: 10,
1320
+ marginTop: 14,
1321
+ marginBottom: 12
1322
+ },
1323
+ children: [
1324
+ /* @__PURE__ */ jsx(
1325
+ "img",
1326
+ {
1327
+ src: authorImage,
1328
+ alt: author.displayName,
1329
+ style: {
1330
+ width: 38,
1331
+ height: 38,
1332
+ borderRadius: 999,
1333
+ objectFit: "cover",
1334
+ boxShadow: `0 0 0 2px ${author.color}22`,
1335
+ flexShrink: 0
1336
+ }
1337
+ }
1338
+ ),
1339
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
1340
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 700, fontSize: 14, color: "#0f172a" }, children: author.displayName }),
1341
+ roomId ? /* @__PURE__ */ jsxs("div", { style: { color: "#64748b", fontSize: 12.5 }, children: [
1342
+ "Sala ",
1343
+ roomId
1344
+ ] }) : null
1345
+ ] })
1346
+ ]
1347
+ }
1348
+ ),
1349
+ /* @__PURE__ */ jsx(
1350
+ "textarea",
1351
+ {
1352
+ ref: commentInputRef,
1353
+ value: composer.body,
1354
+ onChange: (event) => onBodyChange(event.target.value),
1355
+ onKeyDown: handleKeyDown,
1356
+ placeholder,
1357
+ style: {
1358
+ width: "100%",
1359
+ minHeight: 116,
1360
+ padding: "12px 14px",
1361
+ borderRadius: 14,
1362
+ border: "1px solid #cbd5e1",
1363
+ boxSizing: "border-box",
1364
+ fontFamily: "inherit",
1365
+ fontSize: 15,
1366
+ lineHeight: 1.5,
1367
+ color: "#0f172a",
1368
+ resize: "vertical"
1369
+ }
1370
+ }
1371
+ ),
1372
+ /* @__PURE__ */ jsxs(
1373
+ "div",
1374
+ {
1375
+ style: {
1376
+ display: "flex",
1377
+ justifyContent: "space-between",
1378
+ alignItems: "flex-end",
1379
+ flexWrap: "wrap",
1380
+ gap: 10,
1381
+ marginTop: 12
1382
+ },
1383
+ children: [
1384
+ /* @__PURE__ */ jsx(
1385
+ "div",
1386
+ {
1387
+ style: {
1388
+ color: "#64748b",
1389
+ fontSize: 11.5,
1390
+ lineHeight: 1.4,
1391
+ flex: "1 1 120px"
1392
+ },
1393
+ children: submitHint
1394
+ }
1395
+ ),
1396
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, flexShrink: 0 }, children: [
1397
+ /* @__PURE__ */ jsx(
1398
+ "button",
1399
+ {
1400
+ type: "button",
1401
+ onClick: onCancel,
1402
+ style: {
1403
+ padding: "10px 14px",
1404
+ borderRadius: 12,
1405
+ border: "1px solid #cbd5e1",
1406
+ background: "#fff",
1407
+ fontFamily: "inherit",
1408
+ fontSize: 14,
1409
+ fontWeight: 600,
1410
+ whiteSpace: "nowrap",
1411
+ cursor: "pointer"
1412
+ },
1413
+ children: cancelLabel
1414
+ }
1415
+ ),
1416
+ /* @__PURE__ */ jsx(
1417
+ "button",
1418
+ {
1419
+ type: "button",
1420
+ onClick: onCommit,
1421
+ disabled: !composer.body.trim(),
1422
+ style: {
1423
+ padding: "10px 14px",
1424
+ borderRadius: 12,
1425
+ border: "1px solid #6d28d9",
1426
+ background: composer.body.trim() ? "#7c3aed" : "#c4b5fd",
1427
+ color: "#fff",
1428
+ fontFamily: "inherit",
1429
+ fontSize: 14,
1430
+ fontWeight: 600,
1431
+ whiteSpace: "nowrap",
1432
+ cursor: composer.body.trim() ? "pointer" : "not-allowed",
1433
+ opacity: composer.body.trim() ? 1 : 0.72
1434
+ },
1435
+ children: submitLabel
1436
+ }
1437
+ )
1438
+ ] })
1439
+ ]
1440
+ }
1441
+ )
1442
+ ]
1443
+ }
1444
+ );
1445
+ }
1446
+ var overlayShell2 = {
1447
+ position: "absolute",
1448
+ inset: 0,
1449
+ pointerEvents: "none",
1450
+ zIndex: 26,
1451
+ overflow: "hidden"
1452
+ };
1453
+ function renderRealtimeCommentsOverlayNode(options, viewportRef) {
1454
+ return /* @__PURE__ */ jsxs("div", { style: overlayShell2, children: [
1455
+ /* @__PURE__ */ jsx(
1456
+ RealtimeCommentsOverlay,
1457
+ {
1458
+ items: options.items,
1459
+ onItemsChange: options.onItemsChange,
1460
+ isDragEnabled: options.isDragEnabled,
1461
+ cameraVersion: options.cameraVersion,
1462
+ viewportRef
1463
+ }
1464
+ ),
1465
+ /* @__PURE__ */ jsx(
1466
+ RealtimeCommentComposer,
1467
+ {
1468
+ viewportRef,
1469
+ cameraVersion: options.cameraVersion,
1470
+ composer: options.composer,
1471
+ author: options.author,
1472
+ authorImage: options.authorImage,
1473
+ roomId: options.roomId,
1474
+ title: options.title,
1475
+ description: options.description,
1476
+ placeholder: options.placeholder,
1477
+ cancelLabel: options.cancelLabel,
1478
+ submitLabel: options.submitLabel,
1479
+ submitHint: options.submitHint,
1480
+ onBodyChange: options.onBodyChange,
1481
+ onCommit: options.onCommit,
1482
+ onCancel: options.onCancel
1483
+ }
1484
+ )
1485
+ ] });
1486
+ }
1487
+ function useRealtimeComments({
1488
+ items,
1489
+ onItemsChange,
1490
+ author,
1491
+ viewportRef,
1492
+ activeToolId,
1493
+ roomId,
1494
+ setToolId,
1495
+ toolAfterDraft = "select",
1496
+ tools: providedTools = DEFAULT_VECTOR_TOOLS,
1497
+ composerTitle = "Novo coment\xE1rio",
1498
+ composerDescription = "Escreva algo para a equipe. Ao enviar, a bolha sincroniza na sala.",
1499
+ composerPlaceholder = "Contexto, decisao, duvida, feedback...",
1500
+ cancelLabel = "Cancelar",
1501
+ submitLabel = "Publicar coment\xE1rio",
1502
+ submitHint = "`Ctrl/Cmd + Enter` envia"
1503
+ }) {
1504
+ const [cameraVersion, setCameraVersion] = useState(0);
1505
+ const [commentComposer, setCommentComposer] = useState(null);
1506
+ const authorImage = useMemo(
1507
+ () => author.image ?? createRealtimeCommentAvatarDataUrl(author.displayName, author.color),
1508
+ [author.color, author.displayName, author.image]
1509
+ );
1510
+ const tools = useMemo(
1511
+ () => withRealtimeCommentTool(providedTools),
1512
+ [providedTools]
1513
+ );
1514
+ const customPlacement = useMemo(
1515
+ () => ({
1516
+ toolId: "comment",
1517
+ createItem: ({ id, bounds }) => createRealtimeCommentDraftItem(id, bounds, author.color)
1518
+ }),
1519
+ [author.color]
1520
+ );
1521
+ const onViewportItemsChange = useCallback(
1522
+ (nextItems) => {
1523
+ const currentIds = new Set(items.map((item) => item.id));
1524
+ const draftItem = nextItems.find(
1525
+ (item) => !currentIds.has(item.id) && isRealtimeCommentDraftItem(item)
1526
+ );
1527
+ if (!draftItem) {
1528
+ onItemsChange(nextItems);
1529
+ return;
1530
+ }
1531
+ const filteredItems = nextItems.filter((item) => item.id !== draftItem.id);
1532
+ const shouldPersistFiltered = filteredItems.length !== items.length || filteredItems.some((item, index) => items[index]?.id !== item.id);
1533
+ if (shouldPersistFiltered) {
1534
+ onItemsChange(filteredItems);
1535
+ }
1536
+ setCommentComposer({
1537
+ worldX: draftItem.bounds.x + draftItem.bounds.width / 2,
1538
+ worldY: draftItem.bounds.y + draftItem.bounds.height / 2,
1539
+ body: ""
1540
+ });
1541
+ setToolId?.(toolAfterDraft);
1542
+ },
1543
+ [items, onItemsChange, setToolId, toolAfterDraft]
1544
+ );
1545
+ const onCameraChange = useCallback(() => {
1546
+ setCameraVersion((value) => value + 1);
1547
+ }, []);
1548
+ const closeComposer = useCallback(() => {
1549
+ setCommentComposer(null);
1550
+ }, []);
1551
+ const onComposerBodyChange = useCallback((body) => {
1552
+ setCommentComposer((current) => current ? { ...current, body } : current);
1553
+ }, []);
1554
+ const onComposerCommit = useCallback(() => {
1555
+ if (!commentComposer) return;
1556
+ const body = commentComposer.body.trim();
1557
+ if (!body) return;
1558
+ const half = COMMENT_BUBBLE_WORLD_SIZE / 2;
1559
+ const nextItem = createRealtimeCommentItem(
1560
+ createCommentId(),
1561
+ {
1562
+ x: commentComposer.worldX - half,
1563
+ y: commentComposer.worldY - half,
1564
+ width: COMMENT_BUBBLE_WORLD_SIZE,
1565
+ height: COMMENT_BUBBLE_WORLD_SIZE
1566
+ },
1567
+ {
1568
+ body,
1569
+ createdAt: Date.now(),
1570
+ authorPeerId: author.peerId,
1571
+ authorDisplayName: author.displayName,
1572
+ authorColor: author.color,
1573
+ authorImage
1574
+ }
1575
+ );
1576
+ onItemsChange(items.concat(nextItem));
1577
+ setCommentComposer(null);
1578
+ }, [
1579
+ author.color,
1580
+ author.displayName,
1581
+ author.peerId,
1582
+ authorImage,
1583
+ commentComposer,
1584
+ items,
1585
+ onItemsChange
1586
+ ]);
1587
+ const overlay = useMemo(
1588
+ () => renderRealtimeCommentsOverlayNode(
1589
+ {
1590
+ items,
1591
+ onItemsChange,
1592
+ isDragEnabled: activeToolId === "select",
1593
+ cameraVersion,
1594
+ composer: commentComposer,
1595
+ author,
1596
+ authorImage,
1597
+ roomId,
1598
+ title: composerTitle,
1599
+ description: composerDescription,
1600
+ placeholder: composerPlaceholder,
1601
+ cancelLabel,
1602
+ submitLabel,
1603
+ submitHint,
1604
+ onBodyChange: onComposerBodyChange,
1605
+ onCommit: onComposerCommit,
1606
+ onCancel: closeComposer
1607
+ },
1608
+ viewportRef
1609
+ ),
1610
+ [
1611
+ author,
1612
+ authorImage,
1613
+ cameraVersion,
1614
+ cancelLabel,
1615
+ closeComposer,
1616
+ commentComposer,
1617
+ composerDescription,
1618
+ composerPlaceholder,
1619
+ composerTitle,
1620
+ items,
1621
+ activeToolId,
1622
+ onItemsChange,
1623
+ onComposerBodyChange,
1624
+ onComposerCommit,
1625
+ roomId,
1626
+ submitHint,
1627
+ submitLabel,
1628
+ viewportRef
1629
+ ]
1630
+ );
1631
+ const viewport = useMemo(
1632
+ () => ({
1633
+ customPlacement,
1634
+ onItemsChange: onViewportItemsChange,
1635
+ onCameraChange
1636
+ }),
1637
+ [customPlacement, onCameraChange, onViewportItemsChange]
1638
+ );
1639
+ return useMemo(
1640
+ () => ({
1641
+ tools,
1642
+ overlay,
1643
+ viewport,
1644
+ isComposerOpen: commentComposer != null,
1645
+ closeComposer
1646
+ }),
1647
+ [closeComposer, commentComposer, overlay, tools, viewport]
1648
+ );
1649
+ }
1650
+ function createClientId() {
1651
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1652
+ return crypto.randomUUID();
1653
+ }
1654
+ return `client-${Math.random().toString(36).slice(2, 10)}`;
1655
+ }
1656
+ function normalizeSocketUrl(input) {
1657
+ if (input.startsWith("ws://") || input.startsWith("wss://")) return input;
1658
+ if (input.startsWith("http://")) return `ws://${input.slice("http://".length)}`;
1659
+ if (input.startsWith("https://")) return `wss://${input.slice("https://".length)}`;
1660
+ if (input.startsWith("/")) {
1661
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
1662
+ return `${protocol}//${window.location.host}${input}`;
1663
+ }
1664
+ return input;
1665
+ }
1666
+ function isValidSocketUrl(input) {
1667
+ if (!input || input.includes("<") || input.includes(">")) return false;
1668
+ try {
1669
+ const parsed = new URL(input);
1670
+ return parsed.protocol === "ws:" || parsed.protocol === "wss:";
1671
+ } catch {
1672
+ return false;
1673
+ }
1674
+ }
1675
+ function serializeItems(items) {
1676
+ try {
1677
+ return JSON.stringify(items);
1678
+ } catch {
1679
+ return null;
1680
+ }
1681
+ }
1682
+ function sameSerializedItems(left, right) {
1683
+ if (left === right) return true;
1684
+ if (!left || !right) return false;
1685
+ const leftJson = serializeItems(left);
1686
+ const rightJson = serializeItems(right);
1687
+ return leftJson != null && leftJson === rightJson;
1688
+ }
1689
+ function nowMs() {
1690
+ return Date.now();
1691
+ }
1692
+ function useRealtimeSession(options) {
1693
+ const {
1694
+ url,
1695
+ roomId,
1696
+ peer,
1697
+ enabled = true,
1698
+ reconnect = true,
1699
+ heartbeatMs = 1e4,
1700
+ maxReconnectDelayMs = 12e3,
1701
+ initialReconnectDelayMs = 800,
1702
+ connectTimeoutMs = 1e4,
1703
+ onError
1704
+ } = options;
1705
+ const clientIdRef = useRef(peer.clientId ?? createClientId());
1706
+ const wsRef = useRef(null);
1707
+ const reconnectTimerRef = useRef(null);
1708
+ const heartbeatTimerRef = useRef(null);
1709
+ const connectTimeoutRef = useRef(null);
1710
+ const manualDisconnectRef = useRef(false);
1711
+ const retryCountRef = useRef(0);
1712
+ const currentRevisionRef = useRef(0);
1713
+ const outboundInFlightRef = useRef(null);
1714
+ const queuedItemsRef = useRef(null);
1715
+ const subscriberRefs = useRef(/* @__PURE__ */ new Set());
1716
+ const lastCursorRef = useRef(null);
1717
+ const lastMarkupStrokeRef = useRef(null);
1718
+ const lastActiveToolRef = useRef(void 0);
1719
+ const latestDocumentRef = useRef(null);
1720
+ const connectionStateRef = useRef(
1721
+ enabled ? "connecting" : "offline"
1722
+ );
1723
+ const onErrorRef = useRef(onError);
1724
+ onErrorRef.current = onError;
1725
+ const [connectSequence, setConnectSequence] = useState(0);
1726
+ const [connection, setConnection] = useState({
1727
+ state: enabled ? "connecting" : "offline",
1728
+ connected: false,
1729
+ roomId,
1730
+ clientId: null,
1731
+ retryCount: 0,
1732
+ lastConnectedAt: null,
1733
+ lastMessageAt: null,
1734
+ lastPongAt: null,
1735
+ lastError: null
1736
+ });
1737
+ const [sessionPeers, setSessionPeers] = useState([]);
1738
+ const [document2, setDocument] = useState(null);
1739
+ connectionStateRef.current = connection.state;
1740
+ const clearReconnectTimer = useCallback(() => {
1741
+ if (reconnectTimerRef.current != null) {
1742
+ window.clearTimeout(reconnectTimerRef.current);
1743
+ reconnectTimerRef.current = null;
1744
+ }
1745
+ }, []);
1746
+ const clearHeartbeatTimer = useCallback(() => {
1747
+ if (heartbeatTimerRef.current != null) {
1748
+ window.clearInterval(heartbeatTimerRef.current);
1749
+ heartbeatTimerRef.current = null;
1750
+ }
1751
+ }, []);
1752
+ const clearConnectTimeout = useCallback(() => {
1753
+ if (connectTimeoutRef.current != null) {
1754
+ window.clearTimeout(connectTimeoutRef.current);
1755
+ connectTimeoutRef.current = null;
1756
+ }
1757
+ }, []);
1758
+ const updateConnection = useCallback(
1759
+ (patch) => {
1760
+ setConnection((prev) => {
1761
+ if (typeof patch === "function") return patch(prev);
1762
+ return { ...prev, ...patch };
1763
+ });
1764
+ },
1765
+ []
1766
+ );
1767
+ const notifySubscribers = useCallback((items) => {
1768
+ for (const subscriber of subscriberRefs.current) {
1769
+ subscriber(items);
1770
+ }
1771
+ }, []);
1772
+ const applyDocument = useCallback(
1773
+ (snapshot, options2) => {
1774
+ currentRevisionRef.current = snapshot.revision;
1775
+ latestDocumentRef.current = snapshot;
1776
+ setDocument(snapshot);
1777
+ if (!options2?.suppressSubscriberNotify) {
1778
+ notifySubscribers(snapshot.items);
1779
+ }
1780
+ },
1781
+ [notifySubscribers]
1782
+ );
1783
+ const applyPeers = useCallback((peers) => {
1784
+ const selfClientId = clientIdRef.current;
1785
+ setSessionPeers(
1786
+ peers.map(
1787
+ (peerState) => peerState.clientId === selfClientId ? {
1788
+ ...peerState,
1789
+ isSelf: true,
1790
+ connectionState: connectionStateRef.current
1791
+ } : peerState
1792
+ )
1793
+ );
1794
+ }, []);
1795
+ const buildClientMessage = useCallback(
1796
+ (message) => JSON.stringify(message),
1797
+ []
1798
+ );
1799
+ const sendRaw = useCallback(
1800
+ (message) => {
1801
+ const ws = wsRef.current;
1802
+ if (!ws || ws.readyState !== WebSocket.OPEN) return false;
1803
+ ws.send(buildClientMessage(message));
1804
+ return true;
1805
+ },
1806
+ [buildClientMessage]
1807
+ );
1808
+ const flushQueuedDocument = useCallback(() => {
1809
+ const next = queuedItemsRef.current;
1810
+ if (!next || outboundInFlightRef.current) return;
1811
+ queuedItemsRef.current = null;
1812
+ const baseRevision = currentRevisionRef.current;
1813
+ const serialized = serializeItems(next);
1814
+ outboundInFlightRef.current = { baseRevision, items: next, serialized };
1815
+ const didSend = sendRaw({
1816
+ type: "document:update",
1817
+ roomId,
1818
+ clientId: clientIdRef.current,
1819
+ baseRevision,
1820
+ items: next
1821
+ });
1822
+ if (!didSend) {
1823
+ queuedItemsRef.current = next;
1824
+ outboundInFlightRef.current = null;
1825
+ }
1826
+ }, [roomId, sendRaw]);
1827
+ const queueDocumentSend = useCallback(
1828
+ (items) => {
1829
+ queuedItemsRef.current = items;
1830
+ flushQueuedDocument();
1831
+ },
1832
+ [flushQueuedDocument]
1833
+ );
1834
+ const sendPresenceUpdate = useCallback(() => {
1835
+ sendRaw({
1836
+ type: "presence:update",
1837
+ roomId,
1838
+ clientId: clientIdRef.current,
1839
+ presence: {
1840
+ cursor: lastCursorRef.current,
1841
+ markupStroke: lastMarkupStrokeRef.current ?? null,
1842
+ ...lastActiveToolRef.current ? { activeTool: lastActiveToolRef.current } : {}
1843
+ }
1844
+ });
1845
+ }, [roomId, sendRaw]);
1846
+ const scheduleReconnect = useCallback(() => {
1847
+ if (!reconnect || manualDisconnectRef.current) return;
1848
+ clearReconnectTimer();
1849
+ retryCountRef.current += 1;
1850
+ const delay = Math.min(
1851
+ maxReconnectDelayMs,
1852
+ initialReconnectDelayMs * 2 ** Math.max(0, retryCountRef.current - 1)
1853
+ );
1854
+ updateConnection((prev) => ({
1855
+ ...prev,
1856
+ state: "reconnecting",
1857
+ connected: false,
1858
+ retryCount: retryCountRef.current
1859
+ }));
1860
+ reconnectTimerRef.current = window.setTimeout(() => {
1861
+ setConnectSequence((value) => value + 1);
1862
+ }, delay);
1863
+ }, [
1864
+ clearReconnectTimer,
1865
+ initialReconnectDelayMs,
1866
+ maxReconnectDelayMs,
1867
+ reconnect,
1868
+ updateConnection
1869
+ ]);
1870
+ useEffect(() => {
1871
+ if (!enabled) {
1872
+ manualDisconnectRef.current = true;
1873
+ clearReconnectTimer();
1874
+ clearHeartbeatTimer();
1875
+ clearConnectTimeout();
1876
+ wsRef.current?.close();
1877
+ wsRef.current = null;
1878
+ setSessionPeers([]);
1879
+ queuedItemsRef.current = null;
1880
+ outboundInFlightRef.current = null;
1881
+ updateConnection((prev) => ({
1882
+ ...prev,
1883
+ state: "offline",
1884
+ connected: false,
1885
+ roomId,
1886
+ clientId: null
1887
+ }));
1888
+ return;
1889
+ }
1890
+ manualDisconnectRef.current = false;
1891
+ const socketUrl = normalizeSocketUrl(url);
1892
+ if (!isValidSocketUrl(socketUrl)) {
1893
+ updateConnection((prev) => ({
1894
+ ...prev,
1895
+ state: "error",
1896
+ connected: false,
1897
+ roomId,
1898
+ lastError: "URL de websocket invalida."
1899
+ }));
1900
+ onErrorRef.current?.("URL de websocket invalida.");
1901
+ return;
1902
+ }
1903
+ let disposed = false;
1904
+ updateConnection((prev) => ({
1905
+ ...prev,
1906
+ state: retryCountRef.current > 0 ? "reconnecting" : "connecting",
1907
+ connected: false,
1908
+ roomId,
1909
+ lastError: null
1910
+ }));
1911
+ const socket = new WebSocket(socketUrl);
1912
+ wsRef.current = socket;
1913
+ clearConnectTimeout();
1914
+ connectTimeoutRef.current = window.setTimeout(() => {
1915
+ if (socket.readyState === WebSocket.CONNECTING) {
1916
+ socket.close();
1917
+ }
1918
+ }, connectTimeoutMs);
1919
+ socket.addEventListener("open", () => {
1920
+ if (disposed) return;
1921
+ clearConnectTimeout();
1922
+ sendRaw({
1923
+ type: "session:join",
1924
+ roomId,
1925
+ peer: {
1926
+ clientId: clientIdRef.current,
1927
+ peerId: peer.id,
1928
+ ...peer.displayName ? { displayName: peer.displayName } : {},
1929
+ ...peer.color ? { color: peer.color } : {},
1930
+ ...peer.image ? { image: peer.image } : {}
1931
+ }
1932
+ });
1933
+ clearHeartbeatTimer();
1934
+ heartbeatTimerRef.current = window.setInterval(() => {
1935
+ sendRaw({
1936
+ type: "session:ping",
1937
+ roomId,
1938
+ clientId: clientIdRef.current,
1939
+ sentAt: nowMs()
1940
+ });
1941
+ }, heartbeatMs);
1942
+ });
1943
+ socket.addEventListener("message", (event) => {
1944
+ if (disposed) return;
1945
+ let payload = event.data;
1946
+ if (typeof event.data === "string") {
1947
+ try {
1948
+ payload = JSON.parse(event.data);
1949
+ } catch {
1950
+ return;
1951
+ }
1952
+ }
1953
+ const parsed = parseRealtimeServerMessage(payload);
1954
+ if (!parsed) return;
1955
+ updateConnection((prev) => ({
1956
+ ...prev,
1957
+ lastMessageAt: nowMs()
1958
+ }));
1959
+ if (parsed.type === "session:welcome") {
1960
+ retryCountRef.current = 0;
1961
+ const queuedBeforeWelcome = queuedItemsRef.current;
1962
+ const shouldPromoteQueuedLocalDraft = queuedBeforeWelcome != null && parsed.document.revision === 0 && parsed.document.items.length === 0;
1963
+ updateConnection((prev) => ({
1964
+ ...prev,
1965
+ state: "connected",
1966
+ connected: true,
1967
+ clientId: parsed.clientId,
1968
+ retryCount: 0,
1969
+ lastConnectedAt: nowMs(),
1970
+ lastError: null
1971
+ }));
1972
+ applyPeers(parsed.peers);
1973
+ applyDocument(parsed.document, {
1974
+ suppressSubscriberNotify: shouldPromoteQueuedLocalDraft
1975
+ });
1976
+ if (!shouldPromoteQueuedLocalDraft) {
1977
+ queuedItemsRef.current = null;
1978
+ outboundInFlightRef.current = null;
1979
+ }
1980
+ flushQueuedDocument();
1981
+ return;
1982
+ }
1983
+ if (parsed.type === "presence:sync") {
1984
+ applyPeers(parsed.peers);
1985
+ return;
1986
+ }
1987
+ if (parsed.type === "session:peer-joined") {
1988
+ setSessionPeers((prev) => {
1989
+ const next = prev.filter(
1990
+ (peerState) => peerState.clientId !== parsed.peer.clientId
1991
+ );
1992
+ next.push(
1993
+ parsed.peer.clientId === clientIdRef.current ? { ...parsed.peer, isSelf: true } : parsed.peer
1994
+ );
1995
+ return next;
1996
+ });
1997
+ return;
1998
+ }
1999
+ if (parsed.type === "session:peer-left") {
2000
+ setSessionPeers(
2001
+ (prev) => prev.filter((peerState) => peerState.clientId !== parsed.clientId)
2002
+ );
2003
+ return;
2004
+ }
2005
+ if (parsed.type === "session:pong") {
2006
+ updateConnection((prev) => ({
2007
+ ...prev,
2008
+ lastPongAt: parsed.serverTime
2009
+ }));
2010
+ return;
2011
+ }
2012
+ if (parsed.type === "session:error") {
2013
+ updateConnection((prev) => ({
2014
+ ...prev,
2015
+ state: prev.connected ? prev.state : "error",
2016
+ lastError: parsed.message
2017
+ }));
2018
+ onErrorRef.current?.(parsed.message);
2019
+ return;
2020
+ }
2021
+ if (parsed.type === "document:sync") {
2022
+ const selfClientId = clientIdRef.current;
2023
+ const inFlight = outboundInFlightRef.current;
2024
+ const isSelfAck = parsed.document.updatedByClientId === selfClientId;
2025
+ if (!isSelfAck) {
2026
+ outboundInFlightRef.current = null;
2027
+ queuedItemsRef.current = null;
2028
+ applyDocument(parsed.document);
2029
+ return;
2030
+ }
2031
+ const shouldSuppress = inFlight != null && sameSerializedItems(inFlight.items, parsed.document.items);
2032
+ outboundInFlightRef.current = null;
2033
+ applyDocument(parsed.document, { suppressSubscriberNotify: shouldSuppress });
2034
+ if (queuedItemsRef.current) {
2035
+ if (sameSerializedItems(queuedItemsRef.current, parsed.document.items)) {
2036
+ queuedItemsRef.current = null;
2037
+ } else {
2038
+ flushQueuedDocument();
2039
+ }
2040
+ }
2041
+ return;
2042
+ }
2043
+ if (parsed.type === "document:resync-required") {
2044
+ outboundInFlightRef.current = null;
2045
+ queuedItemsRef.current = null;
2046
+ updateConnection((prev) => ({
2047
+ ...prev,
2048
+ lastError: parsed.reason
2049
+ }));
2050
+ applyDocument(parsed.document);
2051
+ }
2052
+ });
2053
+ socket.addEventListener("error", () => {
2054
+ if (disposed) return;
2055
+ updateConnection((prev) => ({
2056
+ ...prev,
2057
+ state: prev.connected ? prev.state : "error",
2058
+ lastError: "Falha de conex\xE3o websocket."
2059
+ }));
2060
+ });
2061
+ socket.addEventListener("close", () => {
2062
+ if (disposed) return;
2063
+ clearHeartbeatTimer();
2064
+ clearConnectTimeout();
2065
+ wsRef.current = null;
2066
+ updateConnection((prev) => ({
2067
+ ...prev,
2068
+ connected: false,
2069
+ clientId: prev.clientId,
2070
+ state: manualDisconnectRef.current || !enabled ? "offline" : prev.state
2071
+ }));
2072
+ if (!manualDisconnectRef.current && enabled) {
2073
+ scheduleReconnect();
2074
+ }
2075
+ });
2076
+ return () => {
2077
+ disposed = true;
2078
+ clearReconnectTimer();
2079
+ clearHeartbeatTimer();
2080
+ clearConnectTimeout();
2081
+ socket.close();
2082
+ };
2083
+ }, [
2084
+ applyDocument,
2085
+ applyPeers,
2086
+ clearConnectTimeout,
2087
+ clearHeartbeatTimer,
2088
+ clearReconnectTimer,
2089
+ connectSequence,
2090
+ connectTimeoutMs,
2091
+ enabled,
2092
+ flushQueuedDocument,
2093
+ heartbeatMs,
2094
+ peer.color,
2095
+ peer.displayName,
2096
+ peer.image,
2097
+ peer.id,
2098
+ roomId,
2099
+ scheduleReconnect,
2100
+ sendRaw,
2101
+ updateConnection,
2102
+ url
2103
+ ]);
2104
+ useEffect(() => {
2105
+ setSessionPeers(
2106
+ (prev) => prev.map(
2107
+ (peerState) => peerState.clientId === clientIdRef.current ? {
2108
+ ...peerState,
2109
+ isSelf: true,
2110
+ connectionState: connection.state
2111
+ } : peerState
2112
+ )
2113
+ );
2114
+ }, [connection.state]);
2115
+ const remoteAdapter = useMemo(
2116
+ () => ({
2117
+ subscribe(onItems) {
2118
+ subscriberRefs.current.add(onItems);
2119
+ if (latestDocumentRef.current) {
2120
+ onItems(latestDocumentRef.current.items);
2121
+ }
2122
+ return () => {
2123
+ subscriberRefs.current.delete(onItems);
2124
+ };
2125
+ },
2126
+ send(items) {
2127
+ queueDocumentSend(items);
2128
+ }
2129
+ }),
2130
+ [queueDocumentSend]
2131
+ );
2132
+ const disconnect = useCallback(() => {
2133
+ manualDisconnectRef.current = true;
2134
+ clearReconnectTimer();
2135
+ clearHeartbeatTimer();
2136
+ clearConnectTimeout();
2137
+ sendRaw({
2138
+ type: "session:leave",
2139
+ roomId,
2140
+ clientId: clientIdRef.current
2141
+ });
2142
+ wsRef.current?.close();
2143
+ wsRef.current = null;
2144
+ updateConnection((prev) => ({
2145
+ ...prev,
2146
+ state: "offline",
2147
+ connected: false
2148
+ }));
2149
+ }, [
2150
+ clearConnectTimeout,
2151
+ clearHeartbeatTimer,
2152
+ clearReconnectTimer,
2153
+ roomId,
2154
+ sendRaw,
2155
+ updateConnection
2156
+ ]);
2157
+ const reconnectNow = useCallback(() => {
2158
+ disconnect();
2159
+ manualDisconnectRef.current = false;
2160
+ retryCountRef.current = 0;
2161
+ setConnectSequence((value) => value + 1);
2162
+ }, [disconnect]);
2163
+ const remotePresence = useMemo(
2164
+ () => sessionPeers.filter((peerState) => !peerState.isSelf),
2165
+ [sessionPeers]
2166
+ );
2167
+ const bindViewportPresence = useCallback(
2168
+ (bindingOptions) => ({
2169
+ remotePresence,
2170
+ onWorldPointerMove(world) {
2171
+ lastCursorRef.current = world;
2172
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2173
+ sendPresenceUpdate();
2174
+ },
2175
+ onWorldPointerLeave() {
2176
+ lastCursorRef.current = null;
2177
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2178
+ sendPresenceUpdate();
2179
+ },
2180
+ onPlacementPreviewChange(preview) {
2181
+ lastMarkupStrokeRef.current = remoteMarkupStrokeFromPlacementPreview(preview);
2182
+ lastActiveToolRef.current = bindingOptions?.activeTool;
2183
+ sendPresenceUpdate();
2184
+ }
2185
+ }),
2186
+ [remotePresence, sendPresenceUpdate]
2187
+ );
2188
+ return {
2189
+ connection,
2190
+ sessionPeers,
2191
+ remotePresence,
2192
+ remoteAdapter,
2193
+ document: document2,
2194
+ bindViewportPresence,
2195
+ disconnect,
2196
+ reconnectNow
2197
+ };
2198
+ }
2199
+ function RealtimeCollaborationPluginComponent({
2200
+ pluginId,
2201
+ options
2202
+ }) {
2203
+ const { viewportRef, viewport } = useCanvuViewportContext();
2204
+ const { items, onItemsChange } = useCanvuDocumentContext();
2205
+ const session = useRealtimeSession(options);
2206
+ const peerDisplayName = options.peer.displayName ?? options.peer.id;
2207
+ const peerColor = options.peer.color ?? defaultPresenceColorForId(options.peer.id);
2208
+ const peerAuthor = useMemo(
2209
+ () => ({
2210
+ peerId: options.peer.id,
2211
+ displayName: peerDisplayName,
2212
+ color: peerColor,
2213
+ ...options.peer.image ? { image: options.peer.image } : {}
2214
+ }),
2215
+ [options.peer.id, options.peer.image, peerColor, peerDisplayName]
2216
+ );
2217
+ const commentOptions = useMemo(
2218
+ () => options.comments === false ? null : options.comments ?? {},
2219
+ [options.comments]
2220
+ );
2221
+ const handleCommentItemsChange = useCallback(
2222
+ (nextItems) => {
2223
+ onItemsChange?.([...nextItems]);
2224
+ session.remoteAdapter.send?.([...nextItems]);
2225
+ },
2226
+ [onItemsChange, session.remoteAdapter]
2227
+ );
2228
+ const comments = useRealtimeComments({
2229
+ items,
2230
+ onItemsChange: handleCommentItemsChange,
2231
+ author: peerAuthor,
2232
+ viewportRef,
2233
+ activeToolId: viewport.toolId,
2234
+ roomId: options.roomId,
2235
+ setToolId: viewport.onToolChangeRequest,
2236
+ ...commentOptions ?? {}
2237
+ });
2238
+ const presenceBindings = useMemo(
2239
+ () => session.bindViewportPresence({ activeTool: viewport.toolId }),
2240
+ [session.bindViewportPresence, viewport.toolId]
2241
+ );
2242
+ useEffect(() => {
2243
+ if (!onItemsChange || !session.document) return;
2244
+ if (session.document.updatedByClientId === session.connection.clientId) return;
2245
+ onItemsChange(session.document.items);
2246
+ }, [onItemsChange, session.connection.clientId, session.document]);
2247
+ const contribution = useMemo(
2248
+ () => ({
2249
+ toolTransform: commentOptions ? () => comments.tools : void 0,
2250
+ customPlacements: commentOptions ? comments.viewport.customPlacement ? [comments.viewport.customPlacement] : void 0 : void 0,
2251
+ viewportProps: {
2252
+ remotePresence: presenceBindings.remotePresence
2253
+ },
2254
+ callbacks: {
2255
+ onWorldPointerMove: presenceBindings.onWorldPointerMove,
2256
+ onWorldPointerLeave: presenceBindings.onWorldPointerLeave,
2257
+ onPlacementPreviewChange: presenceBindings.onPlacementPreviewChange,
2258
+ onCameraChange: commentOptions ? comments.viewport.onCameraChange : void 0
2259
+ },
2260
+ wrapOnItemsChange: (nextItems, ctx) => {
2261
+ if (commentOptions) {
2262
+ comments.viewport.onItemsChange?.(nextItems);
2263
+ return;
2264
+ }
2265
+ ctx.next(nextItems);
2266
+ session.remoteAdapter.send?.(nextItems);
2267
+ }
2268
+ }),
2269
+ [
2270
+ commentOptions,
2271
+ comments.tools,
2272
+ comments.viewport,
2273
+ presenceBindings,
2274
+ session.remoteAdapter
2275
+ ]
2276
+ );
2277
+ useCanvuPluginContribution(pluginId, contribution);
2278
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2279
+ options.showSessionPanel === false ? null : /* @__PURE__ */ jsx(
2280
+ RealtimeSessionPanel,
2281
+ {
2282
+ title: options.sessionPanel?.title ?? "Sess\xE3o realtime",
2283
+ peers: session.sessionPeers,
2284
+ connected: session.connection.connected,
2285
+ connectionState: session.connection.state,
2286
+ roomId: options.roomId,
2287
+ children: options.sessionPanel?.children
2288
+ }
2289
+ ),
2290
+ commentOptions ? comments.overlay : null
2291
+ ] });
2292
+ }
2293
+ function realtimeCollaborationPlugin(options) {
2294
+ const pluginId = `canvu.plugin.realtime-collaboration:${options.roomId}:${options.peer.clientId ?? options.peer.id}`;
2295
+ return createCanvuPlugin({
2296
+ id: pluginId,
2297
+ Component() {
2298
+ return /* @__PURE__ */ jsx(
2299
+ RealtimeCollaborationPluginComponent,
2300
+ {
2301
+ pluginId,
2302
+ options
2303
+ }
2304
+ );
2305
+ }
2306
+ });
2307
+ }
2308
+ function realtimeSessionPlugin(options) {
2309
+ return {
2310
+ id: "trazo.plugin.realtime-session",
2311
+ render: () => /* @__PURE__ */ jsx(RealtimeSessionPanel, { ...options })
2312
+ };
2313
+ }
2314
+
2315
+ export { PresenceRemoteLayer, REALTIME_COMMENT_TOOL, RealtimeCommentsOverlay, RealtimeSessionPanel, createRealtimeCommentAvatarDataUrl, createRealtimeCommentDraftItem, createRealtimeCommentItem, defaultPresenceColorForId, getRealtimeCommentData, isRealtimeCommentDraftItem, isRealtimeCommentItem, parseRealtimeClientMessage, parseRealtimeServerMessage, realtimeCollaborationPlugin, realtimeCommentsPlugin, realtimeSessionPlugin, remoteMarkupStrokeFromPlacementPreview, useRealtimeComments, useRealtimeSession, withRealtimeCommentTool };
2316
+ //# sourceMappingURL=realtime.js.map
2317
+ //# sourceMappingURL=realtime.js.map