kritzel-stencil 0.4.21 → 0.4.22

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 (29) hide show
  1. package/dist/cjs/index.cjs.js +50 -1
  2. package/dist/cjs/kritzel-active-users_47.cjs.entry.js +128 -31
  3. package/dist/collection/classes/providers/sync/hocuspocus-sync-provider.class.js +21 -0
  4. package/dist/collection/classes/providers/sync/websocket-sync-provider.class.js +29 -1
  5. package/dist/collection/classes/structures/app-state-map.structure.js +4 -1
  6. package/dist/collection/classes/structures/object-map.structure.js +66 -4
  7. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +42 -9
  8. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +44 -17
  9. package/dist/collection/constants/version.js +1 -1
  10. package/dist/components/index.js +51 -2
  11. package/dist/components/kritzel-editor.js +16 -11
  12. package/dist/components/kritzel-engine.js +1 -1
  13. package/dist/components/kritzel-settings.js +1 -1
  14. package/dist/components/{p-C_f_QElb.js → p-BNeE3SiZ.js} +113 -21
  15. package/dist/components/{p-BdCrcQ0_.js → p-Dx5NfJ0U.js} +1 -1
  16. package/dist/esm/index.js +50 -1
  17. package/dist/esm/kritzel-active-users_47.entry.js +128 -31
  18. package/dist/stencil/index.esm.js +50 -1
  19. package/dist/stencil/{p-832b80e9.entry.js → p-ab5c4292.entry.js} +128 -31
  20. package/dist/stencil/stencil.esm.js +1 -1
  21. package/dist/types/classes/providers/sync/hocuspocus-sync-provider.class.d.ts +5 -0
  22. package/dist/types/classes/providers/sync/websocket-sync-provider.class.d.ts +6 -0
  23. package/dist/types/classes/structures/object-map.structure.d.ts +26 -2
  24. package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +3 -0
  25. package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +9 -1
  26. package/dist/types/components.d.ts +8 -1
  27. package/dist/types/constants/version.d.ts +1 -1
  28. package/dist/types/interfaces/sync-provider.interface.d.ts +13 -0
  29. package/package.json +1 -1
@@ -570,9 +570,15 @@ class WebSocketSyncProvider {
570
570
  provider;
571
571
  isConnected = false;
572
572
  _quiet = false;
573
+ _isSynced = false;
574
+ _syncedResolvers = [];
573
575
  get awareness() {
574
576
  return this.provider.awareness;
575
577
  }
578
+ /** y-websocket applies remote updates with the underlying provider as the Yjs transaction origin. */
579
+ get updateOrigin() {
580
+ return this.provider;
581
+ }
576
582
  constructor(docName, doc, options) {
577
583
  const url = options?.url || 'ws://localhost:1234';
578
584
  const roomName = options?.roomName || docName;
@@ -619,11 +625,29 @@ class WebSocketSyncProvider {
619
625
  }
620
626
  });
621
627
  this.provider.on('sync', (synced) => {
622
- if (synced && !this._quiet) {
628
+ if (!synced) {
629
+ return;
630
+ }
631
+ this._isSynced = true;
632
+ this.releaseSyncedWaiters();
633
+ if (!this._quiet) {
623
634
  console.info('WebSocket synced');
624
635
  }
625
636
  });
626
637
  }
638
+ releaseSyncedWaiters() {
639
+ const resolvers = this._syncedResolvers;
640
+ this._syncedResolvers = [];
641
+ resolvers.forEach(resolve => resolve());
642
+ }
643
+ async whenSynced() {
644
+ if (this._isSynced || this.provider.synced) {
645
+ return;
646
+ }
647
+ return new Promise(resolve => {
648
+ this._syncedResolvers.push(resolve);
649
+ });
650
+ }
627
651
  async connect() {
628
652
  if (this.isConnected) {
629
653
  return;
@@ -655,6 +679,8 @@ class WebSocketSyncProvider {
655
679
  this.provider.disconnect();
656
680
  }
657
681
  this.isConnected = false;
682
+ this._isSynced = false;
683
+ this.releaseSyncedWaiters();
658
684
  }
659
685
  async reconnect() {
660
686
  this.disconnect();
@@ -665,6 +691,8 @@ class WebSocketSyncProvider {
665
691
  this.provider.destroy();
666
692
  }
667
693
  this.isConnected = false;
694
+ this._isSynced = false;
695
+ this.releaseSyncedWaiters();
668
696
  }
669
697
  }
670
698
 
@@ -686,9 +714,14 @@ class HocuspocusSyncProvider {
686
714
  _connectionStatus = 'disconnected';
687
715
  visibilityHandler = null;
688
716
  onlineHandler = null;
717
+ syncedResolvers = [];
689
718
  get awareness() {
690
719
  return this.provider.awareness;
691
720
  }
721
+ /** HocuspocusProvider applies remote updates with itself as the Yjs transaction origin. */
722
+ get updateOrigin() {
723
+ return this.provider;
724
+ }
692
725
  get connectionStatus() {
693
726
  return this._connectionStatus;
694
727
  }
@@ -745,6 +778,7 @@ class HocuspocusSyncProvider {
745
778
  }
746
779
  this.isSynced = true;
747
780
  this._connectionStatus = 'synced';
781
+ this.releaseSyncedWaiters();
748
782
  if (!options?.quiet) {
749
783
  console.info(`Hocuspocus synced: ${name}`);
750
784
  }
@@ -955,6 +989,19 @@ class HocuspocusSyncProvider {
955
989
  this.disconnect();
956
990
  return this.connect();
957
991
  }
992
+ releaseSyncedWaiters() {
993
+ const resolvers = this.syncedResolvers;
994
+ this.syncedResolvers = [];
995
+ resolvers.forEach(resolve => resolve());
996
+ }
997
+ async whenSynced() {
998
+ if (this.isSynced || this.isDestroyed || this.provider.isSynced) {
999
+ return;
1000
+ }
1001
+ return new Promise(resolve => {
1002
+ this.syncedResolvers.push(resolve);
1003
+ });
1004
+ }
958
1005
  disconnect() {
959
1006
  // Cancel any pending connection attempt
960
1007
  if (this.connectTimeout) {
@@ -976,6 +1023,7 @@ class HocuspocusSyncProvider {
976
1023
  this.isConnected = false;
977
1024
  this.isSynced = false;
978
1025
  this._connectionStatus = 'disconnected';
1026
+ this.releaseSyncedWaiters();
979
1027
  }
980
1028
  destroy() {
981
1029
  // Mark as destroyed first to prevent any callbacks from doing work
@@ -995,6 +1043,7 @@ class HocuspocusSyncProvider {
995
1043
  this.isConnected = false;
996
1044
  this.isSynced = false;
997
1045
  this._connectionStatus = 'disconnected';
1046
+ this.releaseSyncedWaiters();
998
1047
  }
999
1048
  }
1000
1049
 
@@ -2168,6 +2168,8 @@ const KritzelEditor = class {
2168
2168
  this.login = index.createEvent(this, "login");
2169
2169
  this.isPublicChange = index.createEvent(this, "isPublicChange");
2170
2170
  this.awarenessChange = index.createEvent(this, "awarenessChange");
2171
+ this.syncingChange = index.createEvent(this, "syncingChange");
2172
+ this.loadingChange = index.createEvent(this, "loadingChange");
2171
2173
  }
2172
2174
  get host() { return index.getElement(this); }
2173
2175
  scaleMax = ABSOLUTE_SCALE_MAX;
@@ -2407,6 +2409,9 @@ const KritzelEditor = class {
2407
2409
  login;
2408
2410
  isPublicChange;
2409
2411
  awarenessChange;
2412
+ /** Emitted while remote changes are being applied to the active workspace. Stays silent when the remote holds nothing new. */
2413
+ syncingChange;
2414
+ loadingChange;
2410
2415
  isEngineReady = false;
2411
2416
  isControlsReady = false;
2412
2417
  isWorkspaceManagerReady = false;
@@ -3225,40 +3230,40 @@ const KritzelEditor = class {
3225
3230
  const isLoggedIn = this.isLoggedIn;
3226
3231
  const shouldShowCurrentUser = isLoggedIn;
3227
3232
  const shouldShowLoginButton = this.isReady && !!this.loginConfig && !isLoggedIn;
3228
- return (index.h(index.Host, { key: 'd9d87b5ca40972d7ab2c5986f340278347f78c6e' }, index.h("div", { key: 'd8da2ed24d19c550420689c6720dc491fe0d3a22', class: "editor-content", style: {
3233
+ return (index.h(index.Host, { key: 'ff7d505697bd4c4bd840dadcb04a934b7e243a01' }, index.h("div", { key: 'b40076c3c79ad3f879e80c58b412b79266de2ea8', class: "editor-content", style: {
3229
3234
  opacity: this.isEditorVisible ? '1' : '0',
3230
3235
  visibility: this.isEditorVisible ? 'visible' : 'hidden',
3231
3236
  transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out',
3232
- } }, index.h("div", { key: '82a5d216e7c95307b1bdbd1f06fdfe881c1c50d4', class: "top-left-buttons" }, index.h("kritzel-workspace-manager", { key: 'de13673384b44bb6bd309a927a2866a9b78b519e', visible: this.isWorkspaceManagerVisible, workspaces: this.workspaces, activeWorkspace: this.activeWorkspace, terms: this.resolvedTerms, onWorkspaceChange: event => (this.activeWorkspace = event.detail), onIsWorkspaceManagerReady: () => (this.isWorkspaceManagerReady = true) }), index.h("kritzel-back-to-content", { key: 'f68610d7d91713f7a0ec5f0135d208769f7fab9c', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (index.h("div", { key: '9d1f972fb67050b1f996e3088710713fa1219b9a', class: "top-center-notification-layer", role: "presentation" }, index.h("div", { key: '295eff0a02f7cc79ad8e80cda23baa3c576ce156', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, index.h("kritzel-notification-card", { key: '1d1bf140ddfb7b927d30fb1d77e7b887240d7deb', notification: this.activeNotification, locale: this.locale, onDismiss: this.dismissNotification, onHoverChange: event => this.handleNotificationHoverChange(event) })))), this.contextMenuState && this.contextMenuState.items.length > 0 && (index.h("kritzel-context-menu", { key: '1062bc9554e5834ef2002fbfe953f7938866fb84', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
3237
+ } }, index.h("div", { key: '1b1171eea60a36ad06da55a3da564bc356526266', class: "top-left-buttons" }, index.h("kritzel-workspace-manager", { key: '48b59f3f57ace57debc19a295d4fb642e8c3b23d', visible: this.isWorkspaceManagerVisible, workspaces: this.workspaces, activeWorkspace: this.activeWorkspace, terms: this.resolvedTerms, onWorkspaceChange: event => (this.activeWorkspace = event.detail), onIsWorkspaceManagerReady: () => (this.isWorkspaceManagerReady = true) }), index.h("kritzel-back-to-content", { key: 'c66a6faae8bf7115dcce47630ad820af9ba76eb0', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (index.h("div", { key: '834baedc5f20328deb4ee586f693313306895a71', class: "top-center-notification-layer", role: "presentation" }, index.h("div", { key: 'f800508a2e076ee3e89820ffff4df81842b175b3', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, index.h("kritzel-notification-card", { key: '2835ea84e8fa92c41637adaa29d9005f209b2da9', notification: this.activeNotification, locale: this.locale, onDismiss: this.dismissNotification, onHoverChange: event => this.handleNotificationHoverChange(event) })))), this.contextMenuState && this.contextMenuState.items.length > 0 && (index.h("kritzel-context-menu", { key: '6c49e899b7f933d3f06cd1f8696a590d542214a7', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
3233
3238
  position: 'absolute',
3234
3239
  left: `${this.contextMenuState.position.x}px`,
3235
3240
  top: `${this.contextMenuState.position.y}px`,
3236
3241
  zIndex: '10002',
3237
- }, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), index.h("kritzel-sync-indicator", { key: '0094a026828473988f857d82cfccb811f36f049c', visible: this.isSyncing, text: "Syncing..." }), index.h("kritzel-loading-overlay", { key: '680096e20b6175104a866b9a92ca4c1d0087c93a', visible: this.isLoadingOverlayVisible, text: this.resolvedTerms['editor.loading'] ?? 'Loading...' }), index.h("kritzel-engine", { key: '0698a44fcbf2d6ea786d53502350ac0c9ffb9d66', ref: el => {
3242
+ }, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), index.h("kritzel-sync-indicator", { key: '9622af1634a939d6689d5a133496d536f5d7865f', visible: this.isSyncing, text: "Syncing..." }), index.h("kritzel-loading-overlay", { key: '00f142a3eb8b23fbd0bdb6ec53ae95855434516c', visible: this.isLoadingOverlayVisible, text: this.resolvedTerms['editor.loading'] ?? 'Loading...' }), index.h("kritzel-engine", { key: '39b1d7bfce0a5347fec60cd135d6db8fb4250bfa', ref: el => {
3238
3243
  if (el) {
3239
3244
  this.engineRef = el;
3240
3245
  }
3241
- }, workspace: this.activeWorkspace, activeWorkspaceId: this.activeWorkspaceId, editorId: this.editorId, syncConfig: this.syncConfig, assetStorageConfig: this.assetStorageConfig, user: this.user, scaleMax: this.scaleMax, lockDrawingScale: this.lockDrawingScale, isObjectDistanceFadingActive: this.isObjectDistanceFadingActive, scaleMin: this.scaleMin, cursorTarget: this.cursorTarget, isLoading: this.isLoading, viewportBoundaryLeft: this.viewportBoundaryLeft, viewportBoundaryRight: this.viewportBoundaryRight, viewportBoundaryTop: this.viewportBoundaryTop, viewportBoundaryBottom: this.viewportBoundaryBottom, isPanningEnabled: this.isPanningEnabled, isZoomingEnabled: this.isZoomingEnabled, theme: this.theme, themes: this.themes, licenseKey: this.licenseKey, locale: this.locale, locales: this.locales, fallbackLocale: this.fallbackLocale, debugInfo: this.debugInfo, globalContextMenuItems: this.globalContextMenuItems, objectContextMenuItems: this.objectContextMenuItems, onIsEngineReady: event => this.onEngineReady(event), onWorkspacesChange: event => this.handleWorkspacesChange(event), onActiveWorkspaceChange: event => this.handleActiveWorkspaceChange(event), onObjectsChange: event => this.handleObjectsChange(event), onObjectsAdded: event => this.handleObjectsAdded(event), onObjectsRemoved: event => this.handleObjectsRemoved(event), onObjectsUpdated: event => this.handleObjectsUpdated(event), onUndoStateChange: event => this.handleUndoStateChange(event), onObjectsInViewportChange: event => this.handleObjectsInViewportChange(event), onViewportChange: event => this.handleViewportChange(event), onAwarenessChange: event => this.handleAwarenessChange(event), onNotificationsChange: event => this.handleNotificationsChange(event), onContextMenuStateChange: event => this.handleContextMenuStateChange(event), onSyncingChange: event => (this.isSyncing = event.detail), onLoadingChange: event => (this.isLoadingOverlayVisible = event.detail) }), index.h("kritzel-controls", { key: '765a670807111ad68e9f8d14bdca2651a33704d4', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
3246
+ }, workspace: this.activeWorkspace, activeWorkspaceId: this.activeWorkspaceId, editorId: this.editorId, syncConfig: this.syncConfig, assetStorageConfig: this.assetStorageConfig, user: this.user, scaleMax: this.scaleMax, lockDrawingScale: this.lockDrawingScale, isObjectDistanceFadingActive: this.isObjectDistanceFadingActive, scaleMin: this.scaleMin, cursorTarget: this.cursorTarget, isLoading: this.isLoading, viewportBoundaryLeft: this.viewportBoundaryLeft, viewportBoundaryRight: this.viewportBoundaryRight, viewportBoundaryTop: this.viewportBoundaryTop, viewportBoundaryBottom: this.viewportBoundaryBottom, isPanningEnabled: this.isPanningEnabled, isZoomingEnabled: this.isZoomingEnabled, theme: this.theme, themes: this.themes, licenseKey: this.licenseKey, locale: this.locale, locales: this.locales, fallbackLocale: this.fallbackLocale, debugInfo: this.debugInfo, globalContextMenuItems: this.globalContextMenuItems, objectContextMenuItems: this.objectContextMenuItems, onIsEngineReady: event => this.onEngineReady(event), onWorkspacesChange: event => this.handleWorkspacesChange(event), onActiveWorkspaceChange: event => this.handleActiveWorkspaceChange(event), onObjectsChange: event => this.handleObjectsChange(event), onObjectsAdded: event => this.handleObjectsAdded(event), onObjectsRemoved: event => this.handleObjectsRemoved(event), onObjectsUpdated: event => this.handleObjectsUpdated(event), onUndoStateChange: event => this.handleUndoStateChange(event), onObjectsInViewportChange: event => this.handleObjectsInViewportChange(event), onViewportChange: event => this.handleViewportChange(event), onAwarenessChange: event => this.handleAwarenessChange(event), onNotificationsChange: event => this.handleNotificationsChange(event), onContextMenuStateChange: event => this.handleContextMenuStateChange(event), onSyncingChange: event => (this.isSyncing = event.detail), onLoadingChange: event => (this.isLoadingOverlayVisible = event.detail) }), index.h("kritzel-controls", { key: '9a708f21ab92ecc6953a35f5db8001b2403f330e', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
3242
3247
  if (el) {
3243
3248
  this.controlsRef = el;
3244
3249
  }
3245
- }, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), index.h("div", { key: 'e7c5ebab4a40b7ab4fa5d19d203129326c0009ed', class: "bottom-left-buttons" }, index.h("kritzel-zoom-panel", { key: '0aa3891b553d2abcd06e635227fd6bb6656ed739', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), index.h("div", { key: '824244bbd78784c1d78ab89cc12e206ca61d84cc', class: "top-right-buttons" }, index.h("kritzel-settings", { key: 'baf4830b6b9ccb6241b1a1baa5cc6ff9d0295fb9', ref: el => {
3250
+ }, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), index.h("div", { key: '6a2b1cd72099db86952ac31aad9655693d0a6a0d', class: "bottom-left-buttons" }, index.h("kritzel-zoom-panel", { key: 'b3756514bf8de6de8ac7586c626f9759da7f231d', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), index.h("div", { key: 'd5e118bc4d60b867006bac287c5516baf3aa890e', class: "top-right-buttons" }, index.h("kritzel-settings", { key: 'e4065d36beb9283cdbbf3ae9800f4f8011c0b4f0', ref: el => {
3246
3251
  if (el) {
3247
3252
  this.settingsRef = el;
3248
3253
  }
3249
- }, shortcuts: this.shortcuts, availableThemes: this.themes && this.themes.length > 0 ? this.themes.map(t => t.name) : ['light', 'dark'], availableLocales: this.availableLocaleOptions, settings: this.currentSettingsConfig, terms: this.resolvedTerms, onSettingsChange: event => this.handleSettingsChange(event) }), index.h("kritzel-export", { key: '57e2682450c71803cd49bfe65f16e02d9d3358bc', ref: el => {
3254
+ }, shortcuts: this.shortcuts, availableThemes: this.themes && this.themes.length > 0 ? this.themes.map(t => t.name) : ['light', 'dark'], availableLocales: this.availableLocaleOptions, settings: this.currentSettingsConfig, terms: this.resolvedTerms, onSettingsChange: event => this.handleSettingsChange(event) }), index.h("kritzel-export", { key: '4e18d9b42c9e7fef9433577b983c508559461da2', ref: el => {
3250
3255
  if (el) {
3251
3256
  this.exportRef = el;
3252
3257
  }
3253
- }, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), index.h("kritzel-active-users", { key: '955d2a61b98d937f8eec39ab19f1b73e702c9fd7', users: this.activeUsers }), shouldShowCurrentUser && index.h("kritzel-current-user", { key: 'c0885e0c8cab1a4cb50c8f6d4b17e03f32cc2b87', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (index.h("kritzel-current-user-dialog", { key: 'fc70555061d2aeaebdbe981bdbe8cf501bd8120e', ref: el => {
3258
+ }, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), index.h("kritzel-active-users", { key: '56e6b29c9c3dcd7b68af4a1ef7d34224d592ce34', users: this.activeUsers }), shouldShowCurrentUser && index.h("kritzel-current-user", { key: '3b90bcc7e583fcf3c6845d222ad181ccf234a423', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (index.h("kritzel-current-user-dialog", { key: 'f87702c5a708b6afdb2bc3760895e3e3650a4cb5', ref: el => {
3254
3259
  if (el) {
3255
3260
  this.currentUserDialogRef = el;
3256
3261
  }
3257
- }, user: this.user, terms: this.resolvedTerms, onLogoutRequest: this.handleCurrentUserLogout })), shouldShowLoginButton && index.h("kritzel-button", { key: '9666163a2a36a64ecb641659dca515ae30409d0f', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in'), index.h("kritzel-more-menu", { key: '167ac17ea93897597e83d4fe8b3b561fac848bb6', items: this.resolvedMoreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), index.h("kritzel-share-dialog", { key: '10cfb0b794d613ec39299f06ac1a3402d1ad3084', ref: el => {
3262
+ }, user: this.user, terms: this.resolvedTerms, onLogoutRequest: this.handleCurrentUserLogout })), shouldShowLoginButton && index.h("kritzel-button", { key: '015bc0f4c66352136de66d35d28c20070366933f', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in'), index.h("kritzel-more-menu", { key: 'b3fdf9fdb2a34508002e82dfdef9e9bdbbe57ecc', items: this.resolvedMoreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), index.h("kritzel-share-dialog", { key: 'bac4fbbfa04c261e2b128e766cc43c1062aec2ff', ref: el => {
3258
3263
  if (el) {
3259
3264
  this.shareDialogRef = el;
3260
3265
  }
3261
- }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (index.h("kritzel-login-dialog", { key: '99dcb9051382982ec5a8975bdd991bae57745bb9', ref: el => {
3266
+ }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (index.h("kritzel-login-dialog", { key: 'a7d52bad9e752ad546adf80d257c6a46de031e76', ref: el => {
3262
3267
  if (el) {
3263
3268
  this.loginDialogRef = el;
3264
3269
  }
@@ -22301,6 +22306,10 @@ class KritzelObjectMap {
22301
22306
  _awarenessChangeHandler = null;
22302
22307
  _awarenessChangeCallbacks = [];
22303
22308
  _objectsChangeCallbacks = [];
22309
+ _remoteChangesCallbacks = [];
22310
+ _docUpdateHandler = null;
22311
+ _remoteOrigins = new Set();
22312
+ _appliedRemoteChanges = false;
22304
22313
  _lastAwarenessEmitTime = 0;
22305
22314
  _awarenessEmitTimeout = null;
22306
22315
  AWARENESS_THROTTLE_INTERVAL = 100; // milliseconds
@@ -22324,12 +22333,44 @@ class KritzelObjectMap {
22324
22333
  return this._isReady;
22325
22334
  }
22326
22335
  /**
22327
- * Promise that settles when network providers finish connecting.
22328
- * Resolves immediately if no network providers. Awaited by the loading overlay.
22336
+ * Promise that settles when network providers finish connecting and complete
22337
+ * their Yjs sync handshake. Resolves immediately if no network providers.
22329
22338
  */
22330
22339
  whenNetworkSynced() {
22331
22340
  return this._networkSyncPromise ?? Promise.resolve();
22332
22341
  }
22342
+ /**
22343
+ * Whether any configured provider synchronizes over the network.
22344
+ */
22345
+ get hasNetworkSyncProvider() {
22346
+ return this._providers.some(provider => provider.type === 'network');
22347
+ }
22348
+ /**
22349
+ * Whether remote-originated changes can be told apart from local writes.
22350
+ * Requires every network provider to expose its Yjs update origin.
22351
+ */
22352
+ get supportsRemoteChangeDetection() {
22353
+ const networkProviders = this._providers.filter(provider => provider.type === 'network');
22354
+ return networkProviders.length > 0 && networkProviders.every(provider => provider.updateOrigin !== undefined);
22355
+ }
22356
+ /**
22357
+ * Whether at least one update originating from a network provider has been
22358
+ * applied to this document. Stays `false` when the remote held no data the
22359
+ * local document was missing, because Yjs suppresses empty updates.
22360
+ */
22361
+ get hasAppliedRemoteChanges() {
22362
+ return this._appliedRemoteChanges;
22363
+ }
22364
+ /**
22365
+ * Registers a callback invoked whenever a network provider applies changes to this document.
22366
+ * @returns A function that unregisters the callback.
22367
+ */
22368
+ onRemoteChangesApplied(callback) {
22369
+ this._remoteChangesCallbacks.push(callback);
22370
+ return () => {
22371
+ this._remoteChangesCallbacks = this._remoteChangesCallbacks.filter(registered => registered !== callback);
22372
+ };
22373
+ }
22333
22374
  /**
22334
22375
  * Returns the Yjs Awareness instance, if a network provider is available.
22335
22376
  */
@@ -22572,6 +22613,22 @@ class KritzelObjectMap {
22572
22613
  const providerList = this._providers.map(p => `${p.constructor.name} (${p.type})`).join(', ');
22573
22614
  console.info(`[Kritzel] Workspace sync providers initialized for ${docName}: ${providerList || 'none'}`);
22574
22615
  }
22616
+ // Registered before any provider connects so no remote update can be missed.
22617
+ for (const provider of this._providers) {
22618
+ if (provider.type === 'network' && provider.updateOrigin !== undefined) {
22619
+ this._remoteOrigins.add(provider.updateOrigin);
22620
+ }
22621
+ }
22622
+ this._docUpdateHandler = (_update, origin) => {
22623
+ if (!this._remoteOrigins.has(origin)) {
22624
+ return;
22625
+ }
22626
+ this._appliedRemoteChanges = true;
22627
+ for (const callback of [...this._remoteChangesCallbacks]) {
22628
+ callback();
22629
+ }
22630
+ };
22631
+ this._ydoc.on('update', this._docUpdateHandler);
22575
22632
  // captureTimeout is effectively infinite — undo-step boundaries are
22576
22633
  // marked explicitly via markUndoBoundary() in insert/remove/reset and
22577
22634
  // at stroke/gesture boundaries in the tools. This prevents a single
@@ -22625,12 +22682,15 @@ class KritzelObjectMap {
22625
22682
  if (showSyncProviderInfo && networkProviders.length > 0) {
22626
22683
  console.info(`[Kritzel] Workspace network providers connecting in background: ${networkProviders.length}`);
22627
22684
  }
22628
- const networkConnectPromises = networkProviders.map(provider => provider.connect().catch(err => {
22685
+ const networkConnectPromises = networkProviders.map(provider => provider
22686
+ .connect()
22687
+ .then(() => provider.whenSynced?.())
22688
+ .catch(err => {
22629
22689
  if (showSyncProviderInfo) {
22630
22690
  console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
22631
22691
  }
22632
22692
  }));
22633
- // Network sync awaited by loading overlay to show remote workspace hydration
22693
+ // Settles once remote data has been reconciled, not merely once the transport connected
22634
22694
  this._networkSyncPromise = networkConnectPromises.length > 0 ? Promise.allSettled(networkConnectPromises).then(() => undefined) : null;
22635
22695
  this._isReady = true;
22636
22696
  // Find the first provider that exposes awareness (network providers)
@@ -23288,6 +23348,13 @@ class KritzelObjectMap {
23288
23348
  this._objectsMap.unobserve(this._objectsObserver);
23289
23349
  this._objectsObserver = null;
23290
23350
  }
23351
+ if (this._ydoc && this._docUpdateHandler) {
23352
+ this._ydoc.off('update', this._docUpdateHandler);
23353
+ }
23354
+ this._docUpdateHandler = null;
23355
+ this._remoteOrigins.clear();
23356
+ this._remoteChangesCallbacks = [];
23357
+ this._appliedRemoteChanges = false;
23291
23358
  // Remove undo manager event listeners
23292
23359
  if (this._undoManager) {
23293
23360
  if (this._stackItemAddedHandler) {
@@ -23783,7 +23850,10 @@ class KritzelAppStateMap {
23783
23850
  if (showSyncProviderInfo && networkProviders.length > 0) {
23784
23851
  console.info(`[Kritzel] App-state network providers connecting in background: ${networkProviders.length}`);
23785
23852
  }
23786
- const networkConnectPromises = networkProviders.map(provider => provider.connect().catch(err => {
23853
+ const networkConnectPromises = networkProviders.map(provider => provider
23854
+ .connect()
23855
+ .then(() => provider.whenSynced?.())
23856
+ .catch(err => {
23787
23857
  if (showSyncProviderInfo) {
23788
23858
  console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
23789
23859
  }
@@ -27887,7 +27957,7 @@ const KritzelEngine = class {
27887
27957
  contextMenuStateChange;
27888
27958
  /** Emitted when the combined loading state (external `isLoading` prop or internal workspace loading) changes. */
27889
27959
  loadingChange;
27890
- /** Emitted when remote workspace synchronization is in progress. */
27960
+ /** Emitted while remote changes are being applied to the active workspace. Stays silent when the remote holds nothing new. */
27891
27961
  syncingChange;
27892
27962
  forceUpdate = 0;
27893
27963
  isSyncing = false;
@@ -29474,6 +29544,7 @@ const KritzelEngine = class {
29474
29544
  _sceneBootstrapOpacity = 1;
29475
29545
  _sceneBootstrapTransitionEnabled = false;
29476
29546
  _sceneBootstrapTransitionTimer = null;
29547
+ _remoteChangesUnsubscribe = null;
29477
29548
  _defaultUndoState = {
29478
29549
  canUndo: false,
29479
29550
  canRedo: false,
@@ -29491,9 +29562,16 @@ const KritzelEngine = class {
29491
29562
  this.loadingChange.emit(this.core.store.state.isLoading);
29492
29563
  }
29493
29564
  syncSyncingState(isSyncing) {
29565
+ if (this.isSyncing === isSyncing) {
29566
+ return;
29567
+ }
29494
29568
  this.isSyncing = isSyncing;
29495
29569
  this.syncingChange.emit(isSyncing);
29496
29570
  }
29571
+ clearRemoteChangesSubscription() {
29572
+ this._remoteChangesUnsubscribe?.();
29573
+ this._remoteChangesUnsubscribe = null;
29574
+ }
29497
29575
  isNetworkAvailable() {
29498
29576
  return typeof navigator !== 'undefined' && navigator.onLine;
29499
29577
  }
@@ -29605,6 +29683,7 @@ const KritzelEngine = class {
29605
29683
  disconnectedCallback() {
29606
29684
  this.throttledPointerMoveMulti.cancel();
29607
29685
  this._revealedImageKeys.clear();
29686
+ this.clearRemoteChangesSubscription();
29608
29687
  if (this._sceneBootstrapTransitionTimer !== null) {
29609
29688
  clearTimeout(this._sceneBootstrapTransitionTimer);
29610
29689
  this._sceneBootstrapTransitionTimer = null;
@@ -29859,21 +29938,7 @@ const KritzelEngine = class {
29859
29938
  this._workspaceInitializationTargetKey = targetKey;
29860
29939
  try {
29861
29940
  await initializationPromise;
29862
- // Track remote sync in the background without blocking the loading overlay.
29863
- const initializedObjects = this.core.store.objects;
29864
- const networkSyncPromise = initializedObjects?.whenNetworkSynced();
29865
- if (networkSyncPromise && this.isNetworkAvailable()) {
29866
- this.syncSyncingState(true);
29867
- void networkSyncPromise.finally(() => {
29868
- // Only clear syncing for the currently active object map.
29869
- if (this.core.store.objects === initializedObjects) {
29870
- this.syncSyncingState(false);
29871
- }
29872
- });
29873
- }
29874
- else {
29875
- this.syncSyncingState(false);
29876
- }
29941
+ this.trackRemoteSynchronization();
29877
29942
  }
29878
29943
  finally {
29879
29944
  // Only the most recent init clears loading state to prevent race conditions
@@ -29885,6 +29950,38 @@ const KritzelEngine = class {
29885
29950
  }
29886
29951
  }
29887
29952
  }
29953
+ /**
29954
+ * Reports syncing only while remote data is actually being reconciled into the
29955
+ * active workspace. Yjs suppresses updates that carry nothing new, so a workspace
29956
+ * that already matches the remote never reports syncing at all.
29957
+ */
29958
+ trackRemoteSynchronization() {
29959
+ this.clearRemoteChangesSubscription();
29960
+ this.syncSyncingState(false);
29961
+ const initializedObjects = this.core.store.objects;
29962
+ if (!initializedObjects?.hasNetworkSyncProvider || !this.isNetworkAvailable()) {
29963
+ return;
29964
+ }
29965
+ if (initializedObjects.supportsRemoteChangeDetection) {
29966
+ this._remoteChangesUnsubscribe = initializedObjects.onRemoteChangesApplied(() => {
29967
+ if (this.core.store.objects !== initializedObjects) {
29968
+ return;
29969
+ }
29970
+ this.syncSyncingState(true);
29971
+ });
29972
+ }
29973
+ else {
29974
+ // Custom providers without an update origin cannot be diffed, so fall back to reporting the whole handshake.
29975
+ this.syncSyncingState(true);
29976
+ }
29977
+ void initializedObjects.whenNetworkSynced().finally(() => {
29978
+ if (this.core.store.objects !== initializedObjects) {
29979
+ return;
29980
+ }
29981
+ this.clearRemoteChangesSubscription();
29982
+ this.syncSyncingState(false);
29983
+ });
29984
+ }
29888
29985
  emitObjectsChange() {
29889
29986
  const objectsMap = this.core.store.objects;
29890
29987
  if (!objectsMap) {
@@ -32029,7 +32126,7 @@ const KritzelPortal = class {
32029
32126
  * This file is auto-generated by the version bump scripts.
32030
32127
  * Do not modify manually.
32031
32128
  */
32032
- const KRITZEL_VERSION = '0.4.21';
32129
+ const KRITZEL_VERSION = '0.4.22';
32033
32130
 
32034
32131
  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-tab-panel{display:none}.settings-tab-panel.is-active{display:block}.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)}`;
32035
32132
 
@@ -17,9 +17,14 @@ export class HocuspocusSyncProvider {
17
17
  _connectionStatus = 'disconnected';
18
18
  visibilityHandler = null;
19
19
  onlineHandler = null;
20
+ syncedResolvers = [];
20
21
  get awareness() {
21
22
  return this.provider.awareness;
22
23
  }
24
+ /** HocuspocusProvider applies remote updates with itself as the Yjs transaction origin. */
25
+ get updateOrigin() {
26
+ return this.provider;
27
+ }
23
28
  get connectionStatus() {
24
29
  return this._connectionStatus;
25
30
  }
@@ -76,6 +81,7 @@ export class HocuspocusSyncProvider {
76
81
  }
77
82
  this.isSynced = true;
78
83
  this._connectionStatus = 'synced';
84
+ this.releaseSyncedWaiters();
79
85
  if (!options?.quiet) {
80
86
  console.info(`Hocuspocus synced: ${name}`);
81
87
  }
@@ -286,6 +292,19 @@ export class HocuspocusSyncProvider {
286
292
  this.disconnect();
287
293
  return this.connect();
288
294
  }
295
+ releaseSyncedWaiters() {
296
+ const resolvers = this.syncedResolvers;
297
+ this.syncedResolvers = [];
298
+ resolvers.forEach(resolve => resolve());
299
+ }
300
+ async whenSynced() {
301
+ if (this.isSynced || this.isDestroyed || this.provider.isSynced) {
302
+ return;
303
+ }
304
+ return new Promise(resolve => {
305
+ this.syncedResolvers.push(resolve);
306
+ });
307
+ }
289
308
  disconnect() {
290
309
  // Cancel any pending connection attempt
291
310
  if (this.connectTimeout) {
@@ -307,6 +326,7 @@ export class HocuspocusSyncProvider {
307
326
  this.isConnected = false;
308
327
  this.isSynced = false;
309
328
  this._connectionStatus = 'disconnected';
329
+ this.releaseSyncedWaiters();
310
330
  }
311
331
  destroy() {
312
332
  // Mark as destroyed first to prevent any callbacks from doing work
@@ -326,5 +346,6 @@ export class HocuspocusSyncProvider {
326
346
  this.isConnected = false;
327
347
  this.isSynced = false;
328
348
  this._connectionStatus = 'disconnected';
349
+ this.releaseSyncedWaiters();
329
350
  }
330
351
  }
@@ -8,9 +8,15 @@ export class WebSocketSyncProvider {
8
8
  provider;
9
9
  isConnected = false;
10
10
  _quiet = false;
11
+ _isSynced = false;
12
+ _syncedResolvers = [];
11
13
  get awareness() {
12
14
  return this.provider.awareness;
13
15
  }
16
+ /** y-websocket applies remote updates with the underlying provider as the Yjs transaction origin. */
17
+ get updateOrigin() {
18
+ return this.provider;
19
+ }
14
20
  constructor(docName, doc, options) {
15
21
  const url = options?.url || 'ws://localhost:1234';
16
22
  const roomName = options?.roomName || docName;
@@ -57,11 +63,29 @@ export class WebSocketSyncProvider {
57
63
  }
58
64
  });
59
65
  this.provider.on('sync', (synced) => {
60
- if (synced && !this._quiet) {
66
+ if (!synced) {
67
+ return;
68
+ }
69
+ this._isSynced = true;
70
+ this.releaseSyncedWaiters();
71
+ if (!this._quiet) {
61
72
  console.info('WebSocket synced');
62
73
  }
63
74
  });
64
75
  }
76
+ releaseSyncedWaiters() {
77
+ const resolvers = this._syncedResolvers;
78
+ this._syncedResolvers = [];
79
+ resolvers.forEach(resolve => resolve());
80
+ }
81
+ async whenSynced() {
82
+ if (this._isSynced || this.provider.synced) {
83
+ return;
84
+ }
85
+ return new Promise(resolve => {
86
+ this._syncedResolvers.push(resolve);
87
+ });
88
+ }
65
89
  async connect() {
66
90
  if (this.isConnected) {
67
91
  return;
@@ -93,6 +117,8 @@ export class WebSocketSyncProvider {
93
117
  this.provider.disconnect();
94
118
  }
95
119
  this.isConnected = false;
120
+ this._isSynced = false;
121
+ this.releaseSyncedWaiters();
96
122
  }
97
123
  async reconnect() {
98
124
  this.disconnect();
@@ -103,5 +129,7 @@ export class WebSocketSyncProvider {
103
129
  this.provider.destroy();
104
130
  }
105
131
  this.isConnected = false;
132
+ this._isSynced = false;
133
+ this.releaseSyncedWaiters();
106
134
  }
107
135
  }
@@ -120,7 +120,10 @@ export class KritzelAppStateMap {
120
120
  if (showSyncProviderInfo && networkProviders.length > 0) {
121
121
  console.info(`[Kritzel] App-state network providers connecting in background: ${networkProviders.length}`);
122
122
  }
123
- const networkConnectPromises = networkProviders.map(provider => provider.connect().catch(err => {
123
+ const networkConnectPromises = networkProviders.map(provider => provider
124
+ .connect()
125
+ .then(() => provider.whenSynced?.())
126
+ .catch(err => {
124
127
  if (showSyncProviderInfo) {
125
128
  console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
126
129
  }