kritzel-stencil 0.1.74 → 0.1.76

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 (43) hide show
  1. package/dist/cjs/index.cjs.js +131 -86
  2. package/dist/cjs/kritzel-active-users_42.cjs.entry.js +132 -19
  3. package/dist/cjs/{workspace.migrations-Dyt35LBC.js → workspace.migrations-DkmVO6dE.js} +106 -44
  4. package/dist/collection/classes/core/viewport.class.js +32 -3
  5. package/dist/collection/classes/managers/anchor.manager.js +101 -44
  6. package/dist/collection/classes/providers/broadcast-sync-provider.class.js +5 -0
  7. package/dist/collection/classes/providers/hocuspocus-sync-provider.class.js +120 -85
  8. package/dist/collection/classes/providers/indexeddb-sync-provider.class.js +5 -0
  9. package/dist/collection/classes/providers/websocket-sync-provider.class.js +5 -0
  10. package/dist/collection/classes/structures/app-state-map.structure.js +15 -4
  11. package/dist/collection/classes/structures/object-map.structure.js +75 -7
  12. package/dist/collection/components/core/kritzel-awareness-cursors/kritzel-awareness-cursors.css +2 -2
  13. package/dist/collection/components/core/kritzel-awareness-cursors/kritzel-awareness-cursors.js +7 -2
  14. package/dist/collection/constants/version.js +1 -1
  15. package/dist/components/index.js +1 -1
  16. package/dist/components/kritzel-awareness-cursors.js +1 -1
  17. package/dist/components/kritzel-editor.js +1 -1
  18. package/dist/components/kritzel-engine.js +1 -1
  19. package/dist/components/kritzel-settings.js +1 -1
  20. package/dist/components/{p-B4Oqnl55.js → p-31FVoNWR.js} +1 -1
  21. package/dist/components/p-jdYmu4SA.js +9 -0
  22. package/dist/components/p-xNwOWoiT.js +1 -0
  23. package/dist/esm/index.js +132 -87
  24. package/dist/esm/kritzel-active-users_42.entry.js +132 -19
  25. package/dist/esm/{workspace.migrations-B99F1MdT.js → workspace.migrations-D48_Bqvh.js} +106 -44
  26. package/dist/stencil/index.esm.js +1 -1
  27. package/dist/stencil/p-775a7246.entry.js +9 -0
  28. package/dist/stencil/{p-B99F1MdT.js → p-D48_Bqvh.js} +1 -1
  29. package/dist/stencil/stencil.esm.js +1 -1
  30. package/dist/types/classes/core/viewport.class.d.ts +8 -0
  31. package/dist/types/classes/managers/anchor.manager.d.ts +4 -0
  32. package/dist/types/classes/providers/broadcast-sync-provider.class.d.ts +2 -0
  33. package/dist/types/classes/providers/hocuspocus-sync-provider.class.d.ts +37 -1
  34. package/dist/types/classes/providers/indexeddb-sync-provider.class.d.ts +2 -0
  35. package/dist/types/classes/providers/websocket-sync-provider.class.d.ts +2 -0
  36. package/dist/types/classes/structures/object-map.structure.d.ts +6 -0
  37. package/dist/types/constants/version.d.ts +1 -1
  38. package/dist/types/interfaces/remote-cursor.interface.d.ts +1 -0
  39. package/dist/types/interfaces/sync-provider.interface.d.ts +16 -0
  40. package/package.json +1 -1
  41. package/dist/components/p-BSipRoFx.js +0 -1
  42. package/dist/components/p-RJWe82kG.js +0 -9
  43. package/dist/stencil/p-2a60e1bc.entry.js +0 -9
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var workspace_migrations = require('./workspace.migrations-Dyt35LBC.js');
3
+ var workspace_migrations = require('./workspace.migrations-DkmVO6dE.js');
4
4
  var Y = require('yjs');
5
5
  var yWebsocket = require('y-websocket');
6
6
  require('y-indexeddb');
@@ -360,6 +360,7 @@ const readVarUint = decoder => {
360
360
  * This is a lightweight alternative to y-webrtc for browser-tab-only sync
361
361
  */
362
362
  class BroadcastSyncProvider {
363
+ type = 'local';
363
364
  doc;
364
365
  channel;
365
366
  _synced = false;
@@ -441,6 +442,10 @@ class BroadcastSyncProvider {
441
442
  disconnect() {
442
443
  // BroadcastChannel doesn't have explicit disconnect
443
444
  }
445
+ async reconnect() {
446
+ this.disconnect();
447
+ return this.connect();
448
+ }
444
449
  destroy() {
445
450
  this.doc.off('update', this.handleDocUpdate);
446
451
  this.channel.close();
@@ -451,6 +456,7 @@ class BroadcastSyncProvider {
451
456
  * WebSocket sync provider for real-time collaboration
452
457
  */
453
458
  class WebSocketSyncProvider {
459
+ type = 'network';
454
460
  provider;
455
461
  isConnected = false;
456
462
  _quiet = false;
@@ -539,6 +545,10 @@ class WebSocketSyncProvider {
539
545
  }
540
546
  this.isConnected = false;
541
547
  }
548
+ async reconnect() {
549
+ this.disconnect();
550
+ return this.connect();
551
+ }
542
552
  destroy() {
543
553
  if (this.provider) {
544
554
  this.provider.destroy();
@@ -552,6 +562,7 @@ class WebSocketSyncProvider {
552
562
  * Supports multiplexing - multiple documents can share the same WebSocket connection
553
563
  */
554
564
  class HocuspocusSyncProvider {
565
+ type = 'network';
555
566
  provider;
556
567
  isConnected = false;
557
568
  isSynced = false;
@@ -559,16 +570,87 @@ class HocuspocusSyncProvider {
559
570
  isDestroyed = false;
560
571
  connectTimeout = null;
561
572
  pendingConnectReject = null;
573
+ connectionTimeoutMs;
574
+ _connectionStatus = 'disconnected';
575
+ visibilityHandler = null;
576
+ onlineHandler = null;
562
577
  get awareness() {
563
578
  return this.provider.awareness;
564
579
  }
580
+ get connectionStatus() {
581
+ return this._connectionStatus;
582
+ }
565
583
  // Static shared WebSocket instance for multiplexing
566
584
  static sharedWebSocketProvider = null;
567
585
  constructor(docName, doc, options) {
568
586
  const name = options?.name || docName;
569
587
  const url = options?.url || 'ws://localhost:1234';
588
+ this.connectionTimeoutMs = options?.connectionTimeout ?? 10000;
570
589
  // Use provided websocketProvider or the static shared one
571
590
  const websocketProvider = options?.websocketProvider || HocuspocusSyncProvider.sharedWebSocketProvider;
591
+ // Build reconnect config from options
592
+ const reconnectConfig = {};
593
+ if (options?.delay !== undefined)
594
+ reconnectConfig.delay = options.delay;
595
+ if (options?.factor !== undefined)
596
+ reconnectConfig.factor = options.factor;
597
+ if (options?.maxAttempts !== undefined)
598
+ reconnectConfig.maxAttempts = options.maxAttempts;
599
+ if (options?.minDelay !== undefined)
600
+ reconnectConfig.minDelay = options.minDelay;
601
+ if (options?.maxDelay !== undefined)
602
+ reconnectConfig.maxDelay = options.maxDelay;
603
+ const onConnect = () => {
604
+ if (this.isDestroyed) {
605
+ return;
606
+ }
607
+ this.isConnected = true;
608
+ this._connectionStatus = 'connected';
609
+ if (!options?.quiet) {
610
+ console.info(`Hocuspocus connected: ${name}`);
611
+ }
612
+ if (options?.onConnect) {
613
+ options.onConnect();
614
+ }
615
+ };
616
+ const onDisconnect = () => {
617
+ if (this.isDestroyed) {
618
+ return;
619
+ }
620
+ this.isConnected = false;
621
+ this.isSynced = false;
622
+ this._connectionStatus = 'disconnected';
623
+ if (!options?.quiet) {
624
+ console.info(`Hocuspocus disconnected: ${name}`);
625
+ }
626
+ if (options?.onDisconnect) {
627
+ options.onDisconnect();
628
+ }
629
+ };
630
+ const onSynced = () => {
631
+ if (this.isDestroyed) {
632
+ return;
633
+ }
634
+ this.isSynced = true;
635
+ this._connectionStatus = 'synced';
636
+ if (!options?.quiet) {
637
+ console.info(`Hocuspocus synced: ${name}`);
638
+ }
639
+ if (options?.onSynced) {
640
+ options.onSynced();
641
+ }
642
+ };
643
+ const onStatus = (data) => {
644
+ if (this.isDestroyed) {
645
+ return;
646
+ }
647
+ if (data.status === 'connecting') {
648
+ this._connectionStatus = 'connecting';
649
+ }
650
+ if (options?.onStatus) {
651
+ options.onStatus(data);
652
+ }
653
+ };
572
654
  if (websocketProvider) {
573
655
  // Multiplexing mode - use shared WebSocket connection
574
656
  this.usesSharedSocket = true;
@@ -577,48 +659,11 @@ class HocuspocusSyncProvider {
577
659
  name,
578
660
  document: doc,
579
661
  token: options?.token || null,
580
- onStatus: (data) => {
581
- if (options?.onStatus) {
582
- options.onStatus(data);
583
- }
584
- },
585
- onConnect: () => {
586
- if (this.isConnected || this.isDestroyed) {
587
- return;
588
- }
589
- this.isConnected = true;
590
- if (!options?.quiet) {
591
- console.info(`Hocuspocus connected: ${name}`);
592
- }
593
- if (options?.onConnect) {
594
- options.onConnect();
595
- }
596
- },
597
- onDisconnect: () => {
598
- if (this.isDestroyed || (!this.isConnected && !this.isSynced)) {
599
- return;
600
- }
601
- this.isConnected = false;
602
- this.isSynced = false;
603
- if (!options?.quiet) {
604
- console.info(`Hocuspocus disconnected: ${name}`);
605
- }
606
- if (options?.onDisconnect) {
607
- options.onDisconnect();
608
- }
609
- },
610
- onSynced: () => {
611
- if (this.isSynced || this.isDestroyed) {
612
- return;
613
- }
614
- this.isSynced = true;
615
- if (!options?.quiet) {
616
- console.info(`Hocuspocus synced: ${name}`);
617
- }
618
- if (options?.onSynced) {
619
- options.onSynced();
620
- }
621
- },
662
+ onStatus,
663
+ onConnect,
664
+ onDisconnect,
665
+ onSynced,
666
+ ...reconnectConfig,
622
667
  };
623
668
  // Add optional settings
624
669
  if (options?.forceSyncInterval !== undefined) {
@@ -643,48 +688,11 @@ class HocuspocusSyncProvider {
643
688
  document: doc,
644
689
  token: options?.token || null,
645
690
  autoConnect: false,
646
- onStatus: (data) => {
647
- if (options?.onStatus) {
648
- options.onStatus(data);
649
- }
650
- },
651
- onConnect: () => {
652
- if (this.isConnected || this.isDestroyed) {
653
- return;
654
- }
655
- this.isConnected = true;
656
- if (!options?.quiet) {
657
- console.info(`Hocuspocus connected: ${name}`);
658
- }
659
- if (options?.onConnect) {
660
- options.onConnect();
661
- }
662
- },
663
- onDisconnect: () => {
664
- if (this.isDestroyed || (!this.isConnected && !this.isSynced)) {
665
- return;
666
- }
667
- this.isConnected = false;
668
- this.isSynced = false;
669
- if (!options?.quiet) {
670
- console.info(`Hocuspocus disconnected: ${name}`);
671
- }
672
- if (options?.onDisconnect) {
673
- options.onDisconnect();
674
- }
675
- },
676
- onSynced: () => {
677
- if (this.isSynced || this.isDestroyed) {
678
- return;
679
- }
680
- this.isSynced = true;
681
- if (!options?.quiet) {
682
- console.info(`Hocuspocus synced: ${name}`);
683
- }
684
- if (options?.onSynced) {
685
- options.onSynced();
686
- }
687
- },
691
+ onStatus,
692
+ onConnect,
693
+ onDisconnect,
694
+ onSynced,
695
+ ...reconnectConfig,
688
696
  };
689
697
  // Add optional settings
690
698
  if (options?.forceSyncInterval !== undefined) {
@@ -701,6 +709,35 @@ class HocuspocusSyncProvider {
701
709
  console.info(`Hocuspocus Provider initialized: ${url}/${name}`);
702
710
  }
703
711
  }
712
+ this.setupBrowserEventListeners();
713
+ }
714
+ setupBrowserEventListeners() {
715
+ if (typeof document !== 'undefined') {
716
+ this.visibilityHandler = () => {
717
+ if (document.visibilityState === 'visible' && !this.isConnected && !this.isDestroyed) {
718
+ this.provider.connect();
719
+ }
720
+ };
721
+ document.addEventListener('visibilitychange', this.visibilityHandler);
722
+ }
723
+ if (typeof window !== 'undefined') {
724
+ this.onlineHandler = () => {
725
+ if (!this.isConnected && !this.isDestroyed) {
726
+ this.provider.connect();
727
+ }
728
+ };
729
+ window.addEventListener('online', this.onlineHandler);
730
+ }
731
+ }
732
+ removeBrowserEventListeners() {
733
+ if (this.visibilityHandler && typeof document !== 'undefined') {
734
+ document.removeEventListener('visibilitychange', this.visibilityHandler);
735
+ this.visibilityHandler = null;
736
+ }
737
+ if (this.onlineHandler && typeof window !== 'undefined') {
738
+ window.removeEventListener('online', this.onlineHandler);
739
+ this.onlineHandler = null;
740
+ }
704
741
  }
705
742
  /**
706
743
  * Create a shared WebSocket connection for multiplexing
@@ -763,6 +800,7 @@ class HocuspocusSyncProvider {
763
800
  if (this.isSynced || this.isDestroyed) {
764
801
  return;
765
802
  }
803
+ this._connectionStatus = 'connecting';
766
804
  return new Promise((resolve, reject) => {
767
805
  // Store reject function so we can cancel the connection if destroyed
768
806
  this.pendingConnectReject = reject;
@@ -770,7 +808,7 @@ class HocuspocusSyncProvider {
770
808
  this.pendingConnectReject = null;
771
809
  this.connectTimeout = null;
772
810
  reject(new Error('Hocuspocus connection timeout'));
773
- }, 10000); // 10 second timeout
811
+ }, this.connectionTimeoutMs);
774
812
  const syncHandler = () => {
775
813
  if (this.connectTimeout) {
776
814
  clearTimeout(this.connectTimeout);
@@ -800,6 +838,10 @@ class HocuspocusSyncProvider {
800
838
  }
801
839
  });
802
840
  }
841
+ async reconnect() {
842
+ this.disconnect();
843
+ return this.connect();
844
+ }
803
845
  disconnect() {
804
846
  // Cancel any pending connection attempt
805
847
  if (this.connectTimeout) {
@@ -820,6 +862,7 @@ class HocuspocusSyncProvider {
820
862
  }
821
863
  this.isConnected = false;
822
864
  this.isSynced = false;
865
+ this._connectionStatus = 'disconnected';
823
866
  }
824
867
  destroy() {
825
868
  // Mark as destroyed first to prevent any callbacks from doing work
@@ -832,11 +875,13 @@ class HocuspocusSyncProvider {
832
875
  if (this.pendingConnectReject) {
833
876
  this.pendingConnectReject = null; // Don't reject, just abandon the promise
834
877
  }
878
+ this.removeBrowserEventListeners();
835
879
  if (this.provider) {
836
880
  this.provider.destroy();
837
881
  }
838
882
  this.isConnected = false;
839
883
  this.isSynced = false;
884
+ this._connectionStatus = 'disconnected';
840
885
  }
841
886
  }
842
887
 
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var index = require('./index-Dc7LOVhs.js');
4
- var workspace_migrations = require('./workspace.migrations-Dyt35LBC.js');
4
+ var workspace_migrations = require('./workspace.migrations-DkmVO6dE.js');
5
5
  var Y = require('yjs');
6
6
  require('y-websocket');
7
7
  require('y-indexeddb');
@@ -176,7 +176,7 @@ const KritzelAvatar = class {
176
176
  };
177
177
  KritzelAvatar.style = kritzelAvatarCss();
178
178
 
179
- const kritzelAwarenessCursorsCss = () => `:host{display:block;position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:9500}.awareness-cursor{position:absolute;top:0;left:0;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform}.awareness-cursor.stale{opacity:0.3}.awareness-cursor.tracking-object{transition-duration:0ms}.cursor-arrow{filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.cursor-label{position:absolute;left:16px;top:16px;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;user-select:none}.edge-indicator{position:absolute;top:-12px;left:-12px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform;pointer-events:auto;user-select:none;cursor:pointer}.edge-indicator.stale{opacity:0.3}.edge-indicator.tracking-object{transition-duration:0ms}.edge-arrow{position:absolute;filter:drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3))}.edge-label{position:absolute;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;opacity:0;transform-origin:center;transition:opacity 150ms ease}.edge-indicator:hover .edge-label{opacity:1}.remote-selection-box{position:absolute;top:0;left:0;border-width:2px;border-style:solid;pointer-events:none;will-change:transform, width, height;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, width var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, height var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out}`;
179
+ const kritzelAwarenessCursorsCss = () => `:host{display:block;position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:9500}.awareness-cursor{position:absolute;top:0;left:0;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform}.awareness-cursor.stale{opacity:0}.awareness-cursor.tracking-object{transition-duration:0ms}.cursor-arrow{filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.cursor-label{position:absolute;left:16px;top:16px;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;user-select:none}.edge-indicator{position:absolute;top:-12px;left:-12px;width:24px;height:24px;display:flex;align-items:center;justify-content:center;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, opacity 300ms ease;will-change:transform;pointer-events:auto;user-select:none;cursor:pointer}.edge-indicator.stale{opacity:0}.edge-indicator.tracking-object{transition-duration:0ms}.edge-arrow{position:absolute;filter:drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3))}.edge-label{position:absolute;white-space:nowrap;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:var(--kritzel-awareness-cursor-label-font-size, 12px);color:var(--kritzel-awareness-cursor-label-text-color, #ffffff);padding:2px 8px;border-radius:4px;line-height:1.4;font-weight:500;pointer-events:none;opacity:0;transform-origin:center;transition:opacity 150ms ease}.edge-indicator:hover .edge-label{opacity:1}.remote-selection-box{position:absolute;top:0;left:0;border-width:2px;border-style:solid;pointer-events:none;will-change:transform, width, height;transition:transform var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, width var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out, height var(--kritzel-awareness-cursor-transition-duration, 100ms) ease-out}`;
180
180
 
181
181
  const STALE_THRESHOLD_MS = 10_000;
182
182
  const REMOVE_THRESHOLD_MS = 30_000;
@@ -227,6 +227,10 @@ const KritzelAwarenessCursors = class {
227
227
  const cursor = state.cursor;
228
228
  const activeObjectId = state.activeObjectId || null;
229
229
  const selectionBox = state.selectionBox || null;
230
+ const existing = updated.get(clientId);
231
+ const cursorMoved = !existing ||
232
+ !existing.cursor !== !cursor ||
233
+ (cursor && existing.cursor && (cursor.x !== existing.cursor.x || cursor.y !== existing.cursor.y));
230
234
  updated.set(clientId, {
231
235
  clientId,
232
236
  user,
@@ -234,6 +238,7 @@ const KritzelAwarenessCursors = class {
234
238
  activeObjectId,
235
239
  selectionBox,
236
240
  lastUpdated: now,
241
+ lastCursorMove: cursorMoved ? now : (existing?.lastCursorMove ?? now),
237
242
  });
238
243
  });
239
244
  // Remove cursors for disconnected clients
@@ -259,7 +264,7 @@ const KritzelAwarenessCursors = class {
259
264
  }
260
265
  }
261
266
  isStale(cursor) {
262
- return Date.now() - cursor.lastUpdated > STALE_THRESHOLD_MS;
267
+ return Date.now() - cursor.lastCursorMove > STALE_THRESHOLD_MS;
263
268
  }
264
269
  hasActiveDrawingCursors() {
265
270
  for (const cursor of this.remoteCursors.values()) {
@@ -348,7 +353,7 @@ const KritzelAwarenessCursors = class {
348
353
  }
349
354
  render() {
350
355
  const cursors = Array.from(this.remoteCursors.values());
351
- return (index.h(index.Host, { key: 'b8676d9a7a3d4a79ea8ad9763355ecf4c2f90e17' }, cursors.map(remoteCursor => {
356
+ return (index.h(index.Host, { key: '4dd962322c7e955b9038c55cb10f8ffda1e0b246' }, cursors.map(remoteCursor => {
352
357
  if (!remoteCursor.cursor)
353
358
  return null;
354
359
  // When a remote user is actively drawing, derive cursor position from
@@ -19840,6 +19845,14 @@ class KritzelViewport {
19840
19845
  startX = 0;
19841
19846
  /** Starting Y position for pan/zoom gestures */
19842
19847
  startY = 0;
19848
+ /** Minimum movement distance (in screen pixels) before broadcasting touch cursor position */
19849
+ static TOUCH_CURSOR_BROADCAST_THRESHOLD = 5;
19850
+ /** Screen X position where the current touch interaction started */
19851
+ _touchStartScreenX = 0;
19852
+ /** Screen Y position where the current touch interaction started */
19853
+ _touchStartScreenY = 0;
19854
+ /** Whether the touch movement threshold has been exceeded for cursor broadcasting */
19855
+ _touchCursorBroadcastActive = false;
19843
19856
  /**
19844
19857
  * Creates a new KritzelViewport instance and initializes viewport state.
19845
19858
  * Sets up debounced handlers for viewport updates and scaling end events.
@@ -19967,7 +19980,13 @@ class KritzelViewport {
19967
19980
  }
19968
19981
  if (event.pointerType === 'touch' || event.pointerType === 'pen') {
19969
19982
  const activePointers = Array.from(this._core.store.state.pointers.values());
19983
+ if (activePointers.length === 1) {
19984
+ this._touchStartScreenX = event.clientX;
19985
+ this._touchStartScreenY = event.clientY;
19986
+ this._touchCursorBroadcastActive = false;
19987
+ }
19970
19988
  if (activePointers.length === 2) {
19989
+ this._core.store.state.objects?.clearCursorPosition();
19971
19990
  const currentPath = this._core.store.currentPath;
19972
19991
  if (currentPath) {
19973
19992
  this._core.store.state.objects.remove(obj => obj.id === currentPath.id);
@@ -20028,10 +20047,24 @@ class KritzelViewport {
20028
20047
  const hostRect = this._core.store.state.host.getBoundingClientRect();
20029
20048
  const xRelativeToHost = event.clientX - hostRect.left;
20030
20049
  const yRelativeToHost = event.clientY - hostRect.top;
20031
- this._core.store.state.pointerX = (xRelativeToHost - this._core.store.state.translateX) / this._core.store.state.scale;
20032
- this._core.store.state.pointerY = (yRelativeToHost - this._core.store.state.translateY) / this._core.store.state.scale;
20033
- this._core.store.state.objects?.updateCursorPosition(this._core.store.state.pointerX, this._core.store.state.pointerY);
20034
20050
  const activePointers = Array.from(this._core.store.state.pointers.values());
20051
+ if (this._core.store.state.isScaling || activePointers.length > 1) {
20052
+ this._core.store.state.objects?.clearCursorPosition();
20053
+ }
20054
+ else {
20055
+ this._core.store.state.pointerX = (xRelativeToHost - this._core.store.state.translateX) / this._core.store.state.scale;
20056
+ this._core.store.state.pointerY = (yRelativeToHost - this._core.store.state.translateY) / this._core.store.state.scale;
20057
+ if (!this._touchCursorBroadcastActive) {
20058
+ const dx = event.clientX - this._touchStartScreenX;
20059
+ const dy = event.clientY - this._touchStartScreenY;
20060
+ if (Math.sqrt(dx * dx + dy * dy) >= KritzelViewport.TOUCH_CURSOR_BROADCAST_THRESHOLD) {
20061
+ this._touchCursorBroadcastActive = true;
20062
+ }
20063
+ }
20064
+ if (this._touchCursorBroadcastActive) {
20065
+ this._core.store.state.objects?.updateCursorPosition(this._core.store.state.pointerX, this._core.store.state.pointerY);
20066
+ }
20067
+ }
20035
20068
  if (activePointers.length === 2) {
20036
20069
  const firstTouchX = activePointers[0].clientX - this._core.store.offsetX;
20037
20070
  const firstTouchY = activePointers[0].clientY - this._core.store.offsetY;
@@ -20083,6 +20116,7 @@ class KritzelViewport {
20083
20116
  }
20084
20117
  }
20085
20118
  if (event.pointerType === 'touch' || event.pointerType === 'pen') {
20119
+ this._touchCursorBroadcastActive = false;
20086
20120
  if (this._core.store.state.pointers.size === 0) {
20087
20121
  this._debounceEndScaling();
20088
20122
  }
@@ -21362,6 +21396,42 @@ class KritzelObjectMap {
21362
21396
  }
21363
21397
  this._awareness.setLocalStateField('selectionBox', null);
21364
21398
  }
21399
+ /**
21400
+ * Removes selection groups whose owner is no longer present in awareness.
21401
+ * Called when remote clients disconnect to prevent orphaned selection groups
21402
+ * from persisting in the workspace state.
21403
+ */
21404
+ removeOrphanedSelectionGroups() {
21405
+ if (!this._awareness) {
21406
+ return;
21407
+ }
21408
+ const states = this._awareness.getStates();
21409
+ const activeUserIds = new Set();
21410
+ states.forEach(state => {
21411
+ const userId = state.user?.id;
21412
+ if (userId) {
21413
+ activeUserIds.add(userId);
21414
+ }
21415
+ });
21416
+ const localUserId = this._core?.user?.id;
21417
+ const orphanedGroups = this.quadtree.filter(o => o instanceof workspace_migrations.KritzelSelectionGroup
21418
+ && o.userId != null
21419
+ && o.userId !== localUserId
21420
+ && !activeUserIds.has(o.userId));
21421
+ for (const group of orphanedGroups) {
21422
+ this.quadtree.remove(o => o.id === group.id);
21423
+ this._idMap.delete(group.id);
21424
+ if (this._objectsMap) {
21425
+ this._ydoc.transact(() => {
21426
+ this._objectsMap.delete(group.id);
21427
+ }, 'local');
21428
+ }
21429
+ }
21430
+ if (orphanedGroups.length > 0) {
21431
+ this._core?.store.invalidateSelectionCache();
21432
+ this._core?.rerender();
21433
+ }
21434
+ }
21365
21435
  /**
21366
21436
  * Registers a callback to be invoked when the awareness state changes.
21367
21437
  * The callback receives the full awareness states map.
@@ -21495,16 +21565,28 @@ class KritzelObjectMap {
21495
21565
  this.handleObjectsChange(event);
21496
21566
  };
21497
21567
  this._objectsMap.observe(this._objectsObserver);
21498
- // Connect all providers in parallel (settle individually so one failure doesn't block the rest)
21499
- const results = await Promise.allSettled(this._providers.map(p => p.connect()));
21500
- results.forEach((result, i) => {
21568
+ // Separate local providers (IndexedDB, BroadcastChannel) from network providers (Hocuspocus, WebSocket)
21569
+ // Local providers are awaited so data is available immediately; network providers sync in the background
21570
+ // via Yjs observers that are already registered above.
21571
+ const localProviders = this._providers.filter(p => p.type === 'local');
21572
+ const networkProviders = this._providers.filter(p => p.type === 'network');
21573
+ // Await local providers for immediate data availability
21574
+ const localResults = await Promise.allSettled(localProviders.map(p => p.connect()));
21575
+ localResults.forEach((result, i) => {
21501
21576
  if (result.status === 'rejected') {
21502
- console.error(`[Kritzel] Sync provider "${this._providers[i]?.constructor.name}" failed to connect:`, result.reason);
21577
+ console.error(`[Kritzel] Sync provider "${localProviders[i]?.constructor.name}" failed to connect:`, result.reason);
21503
21578
  }
21504
21579
  });
21580
+ // Connect network providers in the background (remote data arrives via Yjs observers)
21581
+ for (const provider of networkProviders) {
21582
+ provider.connect().catch(err => {
21583
+ console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
21584
+ });
21585
+ }
21505
21586
  this._isReady = true;
21506
21587
  // Find the first provider that exposes awareness (network providers)
21507
- for (const provider of this._providers) {
21588
+ // Awareness is available immediately after provider construction, before connect() resolves
21589
+ for (const provider of networkProviders) {
21508
21590
  if (provider.awareness) {
21509
21591
  this._awareness = provider.awareness;
21510
21592
  break;
@@ -21512,7 +21594,11 @@ class KritzelObjectMap {
21512
21594
  }
21513
21595
  // Subscribe to awareness changes
21514
21596
  if (this._awareness) {
21515
- this._awarenessChangeHandler = () => {
21597
+ this._awarenessChangeHandler = (change) => {
21598
+ // Clean up selection groups belonging to disconnected users
21599
+ if (change.removed.length > 0) {
21600
+ this.removeOrphanedSelectionGroups();
21601
+ }
21516
21602
  const now = Date.now();
21517
21603
  const timeSinceLastEmit = now - this._lastAwarenessEmitTime;
21518
21604
  // Clear any pending timeout since we have a new event
@@ -21775,11 +21861,27 @@ class KritzelObjectMap {
21775
21861
  }
21776
21862
  this.quadtree.reset();
21777
21863
  this._idMap.clear();
21778
- this._objectsMap.forEach(serialized => {
21864
+ const localUserId = this._core?.user?.id;
21865
+ const staleSelectionGroupIds = [];
21866
+ this._objectsMap.forEach((serialized, key) => {
21779
21867
  const object = this._reviver.revive(serialized);
21868
+ // Remove remote selection groups on startup — they are transient UI state
21869
+ // that should not survive an app restart. The owning user's session is gone.
21870
+ if (object instanceof workspace_migrations.KritzelSelectionGroup && object.userId != null && object.userId !== localUserId) {
21871
+ staleSelectionGroupIds.push(key);
21872
+ return;
21873
+ }
21780
21874
  this.quadtree.insert(object);
21781
21875
  this._idMap.set(object.id, object);
21782
21876
  });
21877
+ // Clean up stale remote selection groups from Yjs
21878
+ if (staleSelectionGroupIds.length > 0) {
21879
+ this._ydoc.transact(() => {
21880
+ for (const id of staleSelectionGroupIds) {
21881
+ this._objectsMap.delete(id);
21882
+ }
21883
+ }, 'local');
21884
+ }
21783
21885
  }
21784
21886
  /**
21785
21887
  * Resets the object map by clearing both the local quadtree and the Yjs objects map.
@@ -22451,13 +22553,24 @@ class KritzelAppStateMap {
22451
22553
  this.handleWorkspacesChange(event);
22452
22554
  };
22453
22555
  this._workspacesMap.observe(this._workspacesObserver);
22454
- // Connect all providers in parallel (settle individually so one failure doesn't block the rest)
22455
- const results = await Promise.allSettled(this._providers.map(p => p.connect()));
22456
- results.forEach((result, i) => {
22556
+ // Separate local providers (IndexedDB, BroadcastChannel) from network providers (Hocuspocus, WebSocket)
22557
+ // Local providers are awaited so data is available immediately; network providers sync in the background
22558
+ // via Yjs observers that are already registered above.
22559
+ const localProviders = this._providers.filter(p => p.type === 'local');
22560
+ const networkProviders = this._providers.filter(p => p.type === 'network');
22561
+ // Await local providers for immediate data availability
22562
+ const localResults = await Promise.allSettled(localProviders.map(p => p.connect()));
22563
+ localResults.forEach((result, i) => {
22457
22564
  if (result.status === 'rejected') {
22458
- console.error(`[Kritzel] Sync provider "${this._providers[i]?.constructor.name}" failed to connect:`, result.reason);
22565
+ console.error(`[Kritzel] Sync provider "${localProviders[i]?.constructor.name}" failed to connect:`, result.reason);
22459
22566
  }
22460
22567
  });
22568
+ // Connect network providers in the background (remote data arrives via Yjs observers)
22569
+ for (const provider of networkProviders) {
22570
+ provider.connect().catch(err => {
22571
+ console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
22572
+ });
22573
+ }
22461
22574
  this._isReady = true;
22462
22575
  // Run any pending schema migrations before loading data
22463
22576
  const quietMigrations = !core.store.state.debugInfo.showMigrationInfo;
@@ -28408,7 +28521,7 @@ const KritzelPortal = class {
28408
28521
  * This file is auto-generated by the version bump scripts.
28409
28522
  * Do not modify manually.
28410
28523
  */
28411
- const KRITZEL_VERSION = '0.1.74';
28524
+ const KRITZEL_VERSION = '0.1.76';
28412
28525
 
28413
28526
  const kritzelSettingsCss = () => `:host{display:contents}kritzel-dialog{--kritzel-dialog-body-padding:0;--kritzel-dialog-width-large:800px;--kritzel-dialog-height-large:500px}.footer-button{padding:8px 16px;border-radius:6px;cursor:pointer;font-size:14px}.cancel-button{border:1px solid #ebebeb;background:#fff;color:inherit}.cancel-button:hover{background:#f5f5f5}.settings-content{padding:0}.settings-content h3{margin:0 0 16px 0;font-size:18px;font-weight:600;color:var(--kritzel-settings-content-heading-color, #333333)}.settings-content p{margin:0;font-size:14px;color:var(--kritzel-settings-content-text-color, #666666);line-height:1.5}.settings-group{display:flex;flex-direction:column;gap:24px}.settings-item{display:flex;flex-direction:column;gap:8px}.settings-row{display:flex;align-items:center;justify-content:space-between;gap:16px}.settings-label{font-size:14px;font-weight:600;color:var(--kritzel-settings-label-color, #333333);margin:0 0 4px 0}.settings-description{font-size:12px;color:var(--kritzel-settings-description-color, #888888);margin:0;line-height:1.4}.shortcuts-list{display:flex;flex-direction:column;gap:24px}.shortcuts-category{display:flex;flex-direction:column;gap:8px}.shortcuts-category-title{font-size:14px;font-weight:600;color:var(--kritzel-settings-label-color, #333333);margin:0 0 4px 0}.shortcuts-group{display:flex;flex-direction:column;gap:4px}.shortcut-item{display:flex;justify-content:space-between;align-items:center;padding:6px 8px;border-radius:4px;background:var(--kritzel-settings-shortcut-item-bg, rgba(0, 0, 0, 0.02))}.shortcut-label{font-size:14px;color:var(--kritzel-settings-content-text-color, #666666)}.shortcut-key{font-family:monospace;font-size:12px;padding:2px 8px;border-radius:4px;background:var(--kritzel-settings-shortcut-key-bg, #f0f0f0);color:var(--kritzel-settings-shortcut-key-color, #333333);border:1px solid var(--kritzel-settings-shortcut-key-border, #ddd)}`;
28414
28527