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.
- package/dist/cjs/index.cjs.js +131 -86
- package/dist/cjs/kritzel-active-users_42.cjs.entry.js +132 -19
- package/dist/cjs/{workspace.migrations-Dyt35LBC.js → workspace.migrations-DkmVO6dE.js} +106 -44
- package/dist/collection/classes/core/viewport.class.js +32 -3
- package/dist/collection/classes/managers/anchor.manager.js +101 -44
- package/dist/collection/classes/providers/broadcast-sync-provider.class.js +5 -0
- package/dist/collection/classes/providers/hocuspocus-sync-provider.class.js +120 -85
- package/dist/collection/classes/providers/indexeddb-sync-provider.class.js +5 -0
- package/dist/collection/classes/providers/websocket-sync-provider.class.js +5 -0
- package/dist/collection/classes/structures/app-state-map.structure.js +15 -4
- package/dist/collection/classes/structures/object-map.structure.js +75 -7
- package/dist/collection/components/core/kritzel-awareness-cursors/kritzel-awareness-cursors.css +2 -2
- package/dist/collection/components/core/kritzel-awareness-cursors/kritzel-awareness-cursors.js +7 -2
- package/dist/collection/constants/version.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/kritzel-awareness-cursors.js +1 -1
- package/dist/components/kritzel-editor.js +1 -1
- package/dist/components/kritzel-engine.js +1 -1
- package/dist/components/kritzel-settings.js +1 -1
- package/dist/components/{p-B4Oqnl55.js → p-31FVoNWR.js} +1 -1
- package/dist/components/p-jdYmu4SA.js +9 -0
- package/dist/components/p-xNwOWoiT.js +1 -0
- package/dist/esm/index.js +132 -87
- package/dist/esm/kritzel-active-users_42.entry.js +132 -19
- package/dist/esm/{workspace.migrations-B99F1MdT.js → workspace.migrations-D48_Bqvh.js} +106 -44
- package/dist/stencil/index.esm.js +1 -1
- package/dist/stencil/p-775a7246.entry.js +9 -0
- package/dist/stencil/{p-B99F1MdT.js → p-D48_Bqvh.js} +1 -1
- package/dist/stencil/stencil.esm.js +1 -1
- package/dist/types/classes/core/viewport.class.d.ts +8 -0
- package/dist/types/classes/managers/anchor.manager.d.ts +4 -0
- package/dist/types/classes/providers/broadcast-sync-provider.class.d.ts +2 -0
- package/dist/types/classes/providers/hocuspocus-sync-provider.class.d.ts +37 -1
- package/dist/types/classes/providers/indexeddb-sync-provider.class.d.ts +2 -0
- package/dist/types/classes/providers/websocket-sync-provider.class.d.ts +2 -0
- package/dist/types/classes/structures/object-map.structure.d.ts +6 -0
- package/dist/types/constants/version.d.ts +1 -1
- package/dist/types/interfaces/remote-cursor.interface.d.ts +1 -0
- package/dist/types/interfaces/sync-provider.interface.d.ts +16 -0
- package/package.json +1 -1
- package/dist/components/p-BSipRoFx.js +0 -1
- package/dist/components/p-RJWe82kG.js +0 -9
- package/dist/stencil/p-2a60e1bc.entry.js +0 -9
package/dist/cjs/index.cjs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var workspace_migrations = require('./workspace.migrations-
|
|
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
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
},
|
|
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-
|
|
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
|
|
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.
|
|
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: '
|
|
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
|
-
//
|
|
21499
|
-
|
|
21500
|
-
|
|
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 "${
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
22455
|
-
|
|
22456
|
-
|
|
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 "${
|
|
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.
|
|
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
|
|