@q9labsai/chalk-whiteboard 5.0.1 → 5.2.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## 5.2.0
4
+
5
+ ### follow-view-owner
6
+
7
+ Converge Follow views on one Episode owner, recover from its canonical snapshot,
8
+ and preserve real camera moves during recovery and tab switches. Zoom controls
9
+ and keyboard moves now consistently leave Follow and lead the person's view.
10
+
11
+ Keep all 500 active people's views using internal schema-generated limits in the client and React caches. The client's public bounds, event unions and decoded-frame unions are unchanged. The client now exports `ChalkWhiteboardViewVersions`, which the React SDK uses to order view moves, and whiteboard view events now carry `ownerId`, `ownerSequence` and `originEpoch`, so both releases are minor.
12
+
13
+ ## 5.1.0
14
+
15
+ - Add headless mode for hosts that draw their own board controls: `WhiteboardCanvas` accepts `headless`, `inkColor` and `onEngineReady`, and the React entry exports `WhiteboardEngineHandle` (clear, clear with undo, place math), `CANONICAL_INK`, and `observeEngineHistory` with `EngineHistoryState`.
16
+ - Add shared board view events, so collaborators can follow a person's view or bring everyone to theirs.
17
+ - Keep Clear and Undo bound to the scene they were taken from. Redo brings back undone text, and small drawings open at 100% zoom at most.
18
+
19
+ See the [project changelog](../../CHANGELOG.md) for the coordinated v5.1 release.
20
+
21
+ ## 5.0.0
22
+
23
+ - Export `preloadWhiteboardStylesheet` from the React entry point.
24
+ - Keep element order stable across board loads and reconnects.
25
+ - Keep the rest of a board syncing when an element exceeds the wire size limit.
26
+ - BREAKING: remove the retired local image-upload queue and import path. Remote file downloads remain available.
27
+
28
+ See the [project changelog](../../CHANGELOG.md) for the coordinated v5 release.
@@ -0,0 +1,10 @@
1
+ // src/collab/ink.ts
2
+ var CANONICAL_INK = "#1b1c1f";
3
+ function withInk(element, from, to) {
4
+ return from !== to && element.strokeColor.toLowerCase() === from.toLowerCase() ? { ...element, strokeColor: to } : element;
5
+ }
6
+
7
+ export {
8
+ CANONICAL_INK,
9
+ withInk
10
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  compareElementOrder
3
- } from "./chunk-6ZV7GBAC.js";
3
+ } from "./chunk-X3WDTGPQ.js";
4
4
 
5
5
  // src/collab/reducer.ts
6
6
  function mergeWhiteboardElements(current, incoming) {
@@ -1,5 +1,10 @@
1
+ import {
2
+ CANONICAL_INK,
3
+ withInk
4
+ } from "./chunk-IVFWOYSW.js";
5
+
1
6
  // src/collab/engine.ts
2
- import { CaptureUpdateAction as CaptureUpdateAction2, reconcileElements, restoreElements } from "@excalidraw/excalidraw";
7
+ import { CaptureUpdateAction as CaptureUpdateAction2, newElementWith as newElementWith2, reconcileElements, restoreElements } from "@excalidraw/excalidraw";
3
8
 
4
9
  // src/collab/camera.ts
5
10
  function getWhiteboardCamera(appState) {
@@ -193,7 +198,7 @@ var toSocketId = (value) => value;
193
198
  var WhiteboardPresence = class {
194
199
  constructor(opts) {
195
200
  this.opts = opts;
196
- this.pruneInterval = setInterval(() => this.pruneStale(), 1e3);
201
+ if (opts.renderCursors) this.pruneInterval = setInterval(() => this.pruneStale(), 1e3);
197
202
  }
198
203
  opts;
199
204
  lastCursorSend = 0;
@@ -211,6 +216,7 @@ var WhiteboardPresence = class {
211
216
  this.opts.sendCursor(payload.pointer);
212
217
  }
213
218
  handleRemoteCursor(payload) {
219
+ if (!this.opts.renderCursors) return;
214
220
  const ts = payload.timestamp instanceof Date ? payload.timestamp.getTime() : Date.now();
215
221
  this.collaborators.set(payload.participantId, {
216
222
  pointer: { x: payload.x, y: payload.y, tool: "pointer", renderCursor: true },
@@ -399,6 +405,8 @@ var ExcalidrawCollabEngine = class {
399
405
  constructor(opts) {
400
406
  this.opts = opts;
401
407
  this.canDraw = opts.canDraw;
408
+ this.canManage = opts.canManage;
409
+ this.inkColor = opts.inkColor ?? CANONICAL_INK;
402
410
  this.filesSync = new WhiteboardFilesSync({
403
411
  excalidrawAPI: opts.excalidrawAPI,
404
412
  fileTransfer: opts.fileTransfer,
@@ -409,6 +417,7 @@ var ExcalidrawCollabEngine = class {
409
417
  this.presence = new WhiteboardPresence({
410
418
  excalidrawAPI: opts.excalidrawAPI,
411
419
  sendCursor: opts.sendCursor,
420
+ renderCursors: opts.renderCursors ?? true,
412
421
  throttleMs: CURSOR_THROTTLE_MS,
413
422
  staleMs: CURSOR_STALE_MS
414
423
  });
@@ -417,8 +426,11 @@ var ExcalidrawCollabEngine = class {
417
426
  }
418
427
  opts;
419
428
  sceneId = null;
429
+ sceneRevision = null;
420
430
  sceneGeneration = null;
421
431
  canDraw = true;
432
+ canManage;
433
+ inkColor;
422
434
  storedImageFileIds = /* @__PURE__ */ new Set();
423
435
  broadcastedElementVersions = /* @__PURE__ */ new Map();
424
436
  rejectedElementVersions = /* @__PURE__ */ new Map();
@@ -437,11 +449,24 @@ var ExcalidrawCollabEngine = class {
437
449
  setCanDraw(next) {
438
450
  this.canDraw = next;
439
451
  }
452
+ setCanManage(next) {
453
+ this.canManage = next;
454
+ }
455
+ /** Repaints Ink for a new surface without a new version, so nothing is sent. */
456
+ setInkColor(next) {
457
+ const previous = this.inkColor;
458
+ if (next === previous) return;
459
+ this.inkColor = next;
460
+ const api = this.opts.excalidrawAPI;
461
+ api.updateScene({ elements: api.getSceneElementsIncludingDeleted().map((element) => withInk(element, previous, next)), captureUpdate: CaptureUpdateAction2.NEVER });
462
+ api.history.clear();
463
+ }
440
464
  async clear() {
441
- if (!this.canDraw) throw new Error("whiteboard drawing is not permitted");
465
+ if (!(this.canManage ?? this.canDraw)) throw new Error("whiteboard management is not permitted");
442
466
  const commit = await this.opts.clear();
443
467
  this.storedImageFileIds.clear();
444
468
  this.sceneId = commit.sceneId;
469
+ this.sceneRevision = commit.revision;
445
470
  this.sceneGeneration = commit.sceneGeneration ?? null;
446
471
  this.broadcastedElementVersions.clear();
447
472
  this.rejectedElementVersions.clear();
@@ -451,8 +476,20 @@ var ExcalidrawCollabEngine = class {
451
476
  elements: [],
452
477
  captureUpdate: CaptureUpdateAction2.NEVER
453
478
  });
479
+ this.opts.excalidrawAPI.history.clear();
454
480
  return commit;
455
481
  }
482
+ /** Restore only into the exact empty scene committed by this clear. */
483
+ async clearWithUndo() {
484
+ const api = this.opts.excalidrawAPI;
485
+ const before = api.getSceneElements();
486
+ const commit = await this.clear();
487
+ return () => {
488
+ if (this.disposed || !this.canDraw || this.sceneId !== commit.sceneId || this.sceneRevision !== commit.revision || this.sceneGeneration !== (commit.sceneGeneration ?? null) || api.getSceneElementsIncludingDeleted().length > 0) return false;
489
+ api.updateScene({ elements: before.map((element) => newElementWith2(element, {})), captureUpdate: CaptureUpdateAction2.IMMEDIATELY });
490
+ return true;
491
+ };
492
+ }
456
493
  dispose() {
457
494
  if (this.disposed) return;
458
495
  const hasPendingChange = this.changeDebounce !== null || this.dirtyDuringSubmission || this.submissionRetryTimer !== null;
@@ -497,6 +534,7 @@ var ExcalidrawCollabEngine = class {
497
534
  case "snapshot":
498
535
  this.handleRemoteSnapshot({
499
536
  sceneId: event.sceneId,
537
+ revision: event.revision,
500
538
  ...event.sceneGeneration ? { sceneGeneration: event.sceneGeneration } : {},
501
539
  elements: event.elements,
502
540
  appState: event.appState
@@ -505,6 +543,7 @@ var ExcalidrawCollabEngine = class {
505
543
  case "update":
506
544
  this.handleRemoteData({
507
545
  sceneId: event.sceneId,
546
+ revision: event.revision,
508
547
  ...event.sceneGeneration ? { sceneGeneration: event.sceneGeneration } : {},
509
548
  syncAll: false,
510
549
  elements: event.elements
@@ -519,25 +558,30 @@ var ExcalidrawCollabEngine = class {
519
558
  timestamp: new Date(event.occurredAt)
520
559
  });
521
560
  return;
561
+ case "view":
562
+ return;
522
563
  case "reset_required":
564
+ this.sceneRevision = null;
523
565
  this.requestSnapshot();
524
566
  }
525
567
  }
526
568
  handleRemoteData(payload) {
527
569
  this.applyRemoteElements({
528
570
  sceneId: payload.sceneId,
571
+ revision: payload.revision,
529
572
  ...payload.sceneGeneration ? { sceneGeneration: payload.sceneGeneration } : {},
530
573
  syncAll: payload.syncAll,
531
- remoteElements: payload.elements.map(fromWireElement),
574
+ remoteElements: payload.elements.map((element) => withInk(fromWireElement(element), CANONICAL_INK, this.inkColor)),
532
575
  isSnapshot: false
533
576
  });
534
577
  }
535
578
  handleRemoteSnapshot(payload) {
536
579
  this.applyRemoteElements({
537
580
  sceneId: payload.sceneId,
581
+ revision: payload.revision,
538
582
  ...payload.sceneGeneration ? { sceneGeneration: payload.sceneGeneration } : {},
539
583
  syncAll: true,
540
- remoteElements: payload.elements.map(fromWireElement),
584
+ remoteElements: payload.elements.map((element) => withInk(fromWireElement(element), CANONICAL_INK, this.inkColor)),
541
585
  appState: payload.appState,
542
586
  isSnapshot: true
543
587
  });
@@ -645,6 +689,7 @@ var ExcalidrawCollabEngine = class {
645
689
  this.requestSnapshot();
646
690
  return;
647
691
  }
692
+ this.sceneRevision = commit.revision;
648
693
  for (const element of submittedElements) {
649
694
  const previous = this.broadcastedElementVersions.get(element.id);
650
695
  const submittedVersion = { version: element.version, versionNonce: element.version_nonce };
@@ -685,7 +730,7 @@ var ExcalidrawCollabEngine = class {
685
730
  const submitted = [];
686
731
  for (const element of elements) {
687
732
  try {
688
- submitted.push(toFittedWireElement(element));
733
+ submitted.push(toFittedWireElement(withInk(element, this.inkColor, CANONICAL_INK)));
689
734
  this.rejectedElementVersions.delete(element.id);
690
735
  } catch (cause) {
691
736
  const reported = this.rejectedElementVersions.has(element.id);
@@ -723,12 +768,17 @@ var ExcalidrawCollabEngine = class {
723
768
  this.deferredRemoteElements.clear();
724
769
  this.resetSubmissionRetries();
725
770
  }
771
+ this.sceneRevision = args.revision ?? null;
726
772
  if (args.sceneGeneration) this.sceneGeneration = args.sceneGeneration;
727
773
  const excalidrawAPI = this.opts.excalidrawAPI;
728
774
  if (replacingScene) excalidrawAPI.history.clear();
729
775
  const local = replacingScene ? [] : excalidrawAPI.getSceneElementsIncludingDeleted();
730
776
  const remoteElements = [...args.remoteElements].sort(compareElementOrder);
731
- const restoredRemote = restoreElements(remoteElements, null);
777
+ const received = new Map(remoteElements.map((element) => [element.id, element]));
778
+ const restoredRemote = restoreElements(remoteElements, null).map((element) => {
779
+ const remote = received.get(element.id);
780
+ return remote && !remote.isDeleted && element.isDeleted ? { ...element, isDeleted: false, version: remote.version, versionNonce: remote.versionNonce } : element;
781
+ });
732
782
  this.storedImageFileIds = /* @__PURE__ */ new Set([...this.storedImageFileIds, ...remoteImageFileIds(restoredRemote)]);
733
783
  const remoteForReconcile = toReconcileRemoteElements(restoredRemote);
734
784
  const appState = excalidrawAPI.getAppState();
@@ -22,7 +22,31 @@ export type WhiteboardCollaborationEvent = {
22
22
  readonly x: number;
23
23
  readonly y: number;
24
24
  readonly occurredAt: string;
25
+ } | ({
26
+ readonly type: "view";
27
+ readonly participantId: string;
28
+ readonly displayName: string;
29
+ readonly x: number;
30
+ readonly y: number;
31
+ readonly width: number;
32
+ readonly height: number;
33
+ readonly bring: boolean;
34
+ readonly occurredAt: string;
35
+ } & ({
36
+ readonly originParticipantId: string;
37
+ readonly originSequence: number;
38
+ readonly originEpoch?: string;
39
+ } | {
40
+ readonly originParticipantId?: never;
41
+ readonly originSequence?: never;
42
+ readonly originEpoch?: never;
43
+ }) & ({
44
+ readonly ownerId: string;
45
+ readonly ownerSequence: number;
25
46
  } | {
47
+ readonly ownerId?: never;
48
+ readonly ownerSequence?: never;
49
+ })) | {
26
50
  readonly type: "reset_required";
27
51
  readonly sceneId: string;
28
52
  readonly reason: "scene_changed" | "cursor_expired" | "gap";
@@ -30,8 +54,11 @@ export type WhiteboardCollaborationEvent = {
30
54
  export interface ExcalidrawCollabEngineOptions extends WhiteboardFileTransferOptions {
31
55
  excalidrawAPI: ExcalidrawImperativeAPI;
32
56
  canDraw: boolean;
57
+ /** Explicit management authority; omitted hosts keep the original draw-based behavior. */
58
+ canManage?: boolean;
33
59
  submitUpdate: (payload: {
34
60
  sceneId: string;
61
+ revision?: string;
35
62
  sceneGeneration?: string;
36
63
  syncAll: boolean;
37
64
  elements: readonly WhiteboardWireElement[];
@@ -45,12 +72,19 @@ export interface ExcalidrawCollabEngineOptions extends WhiteboardFileTransferOpt
45
72
  subscribe: (listener: (event: WhiteboardCollaborationEvent) => void) => () => void;
46
73
  onFileSyncStateChange?: (state: WhiteboardFileSyncState) => void;
47
74
  onSubmissionError?: (error: unknown) => void;
75
+ /** False when the host draws its own remote cursors. */
76
+ renderCursors?: boolean;
77
+ /** The colour this viewer sees for Ink, which travels as one canonical colour. */
78
+ inkColor?: string;
48
79
  }
49
80
  export declare class ExcalidrawCollabEngine {
50
81
  private readonly opts;
51
82
  private sceneId;
83
+ private sceneRevision;
52
84
  private sceneGeneration;
53
85
  private canDraw;
86
+ private canManage;
87
+ private inkColor;
54
88
  private storedImageFileIds;
55
89
  private broadcastedElementVersions;
56
90
  private rejectedElementVersions;
@@ -68,7 +102,12 @@ export declare class ExcalidrawCollabEngine {
68
102
  private readonly unsubscribe;
69
103
  constructor(opts: ExcalidrawCollabEngineOptions);
70
104
  setCanDraw(next: boolean): void;
105
+ setCanManage(next: boolean | undefined): void;
106
+ /** Repaints Ink for a new surface without a new version, so nothing is sent. */
107
+ setInkColor(next: string): void;
71
108
  clear(): Promise<WhiteboardCommit>;
109
+ /** Restore only into the exact empty scene committed by this clear. */
110
+ clearWithUndo(): Promise<() => boolean>;
72
111
  dispose(): void;
73
112
  handleChange(_elements: readonly OrderedExcalidrawElement[], _appState: AppState, _files: BinaryFiles): void;
74
113
  handlePointerUpdate(payload: {
@@ -87,12 +126,14 @@ export declare class ExcalidrawCollabEngine {
87
126
  handleRemoteEvent(event: WhiteboardCollaborationEvent): void;
88
127
  handleRemoteData(payload: {
89
128
  sceneId: string;
129
+ revision?: string;
90
130
  sceneGeneration?: string;
91
131
  syncAll: boolean;
92
132
  elements: readonly WhiteboardWireElement[];
93
133
  }): void;
94
134
  handleRemoteSnapshot(payload: {
95
135
  sceneId: string;
136
+ revision?: string;
96
137
  sceneGeneration?: string;
97
138
  elements: readonly WhiteboardWireElement[];
98
139
  appState?: SharedWhiteboardAppState;
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  mergeWhiteboardElements
3
- } from "../chunk-W4FLPXAX.js";
3
+ } from "../chunk-MPNIDZHM.js";
4
4
  import {
5
5
  ExcalidrawCollabEngine,
6
6
  fromWireElement,
7
7
  isWireElement,
8
8
  toWireElement
9
- } from "../chunk-6ZV7GBAC.js";
9
+ } from "../chunk-X3WDTGPQ.js";
10
+ import "../chunk-IVFWOYSW.js";
10
11
  export {
11
12
  ExcalidrawCollabEngine,
12
13
  fromWireElement,
@@ -0,0 +1,5 @@
1
+ /** Ink travels and records as Paper's ink; each viewer paints it in their own surface's ink. */
2
+ export declare const CANONICAL_INK = "#1b1c1f";
3
+ export declare function withInk<T extends {
4
+ readonly strokeColor: string;
5
+ }>(element: T, from: string, to: string): T;
@@ -10,6 +10,7 @@ export declare class WhiteboardPresence {
10
10
  x: number;
11
11
  y: number;
12
12
  }) => void;
13
+ renderCursors: boolean;
13
14
  throttleMs: number;
14
15
  staleMs: number;
15
16
  });
@@ -1216,8 +1216,8 @@
1216
1216
  },
1217
1217
  {
1218
1218
  "path": "renderer.js",
1219
- "bytes": 8497928,
1220
- "sha256": "7f754073f20cee4b3adceb40731faf75d94849f6ce01a0b2e30fdad10867a5d9"
1219
+ "bytes": 8502340,
1220
+ "sha256": "82afcc03b426364bcf0e83064452972ec6fc5b74f766fd024948ae866893ba68"
1221
1221
  },
1222
1222
  {
1223
1223
  "path": "THIRD_PARTY_NOTICES.json",