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
@@ -2146,6 +2146,8 @@ const KritzelEditor = class {
2146
2146
  this.login = createEvent(this, "login");
2147
2147
  this.isPublicChange = createEvent(this, "isPublicChange");
2148
2148
  this.awarenessChange = createEvent(this, "awarenessChange");
2149
+ this.syncingChange = createEvent(this, "syncingChange");
2150
+ this.loadingChange = createEvent(this, "loadingChange");
2149
2151
  }
2150
2152
  get host() { return getElement(this); }
2151
2153
  scaleMax = ABSOLUTE_SCALE_MAX;
@@ -2385,6 +2387,9 @@ const KritzelEditor = class {
2385
2387
  login;
2386
2388
  isPublicChange;
2387
2389
  awarenessChange;
2390
+ /** Emitted while remote changes are being applied to the active workspace. Stays silent when the remote holds nothing new. */
2391
+ syncingChange;
2392
+ loadingChange;
2388
2393
  isEngineReady = false;
2389
2394
  isControlsReady = false;
2390
2395
  isWorkspaceManagerReady = false;
@@ -3203,40 +3208,40 @@ const KritzelEditor = class {
3203
3208
  const isLoggedIn = this.isLoggedIn;
3204
3209
  const shouldShowCurrentUser = isLoggedIn;
3205
3210
  const shouldShowLoginButton = this.isReady && !!this.loginConfig && !isLoggedIn;
3206
- return (h(Host, { key: 'd9d87b5ca40972d7ab2c5986f340278347f78c6e' }, h("div", { key: 'd8da2ed24d19c550420689c6720dc491fe0d3a22', class: "editor-content", style: {
3211
+ return (h(Host, { key: 'ff7d505697bd4c4bd840dadcb04a934b7e243a01' }, h("div", { key: 'b40076c3c79ad3f879e80c58b412b79266de2ea8', class: "editor-content", style: {
3207
3212
  opacity: this.isEditorVisible ? '1' : '0',
3208
3213
  visibility: this.isEditorVisible ? 'visible' : 'hidden',
3209
3214
  transition: 'opacity 0.2s ease-in-out, visibility 0.2s ease-in-out',
3210
- } }, h("div", { key: '82a5d216e7c95307b1bdbd1f06fdfe881c1c50d4', class: "top-left-buttons" }, 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) }), h("kritzel-back-to-content", { key: 'f68610d7d91713f7a0ec5f0135d208769f7fab9c', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (h("div", { key: '9d1f972fb67050b1f996e3088710713fa1219b9a', class: "top-center-notification-layer", role: "presentation" }, h("div", { key: '295eff0a02f7cc79ad8e80cda23baa3c576ce156', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, 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 && (h("kritzel-context-menu", { key: '1062bc9554e5834ef2002fbfe953f7938866fb84', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
3215
+ } }, h("div", { key: '1b1171eea60a36ad06da55a3da564bc356526266', class: "top-left-buttons" }, 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) }), h("kritzel-back-to-content", { key: 'c66a6faae8bf7115dcce47630ad820af9ba76eb0', visible: this.isBackToContentButtonVisible, text: this.resolvedTerms['backToContent.label'] ?? 'Back to content', onBackToContent: () => this.backToContent() })), this.activeNotification && (h("div", { key: '834baedc5f20328deb4ee586f693313306895a71', class: "top-center-notification-layer", role: "presentation" }, h("div", { key: 'f800508a2e076ee3e89820ffff4df81842b175b3', class: { 'top-center-notification-container': true, 'is-dismissing': this.isNotificationDismissing }, role: "presentation" }, 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 && (h("kritzel-context-menu", { key: '6c49e899b7f933d3f06cd1f8696a590d542214a7', class: "context-menu", items: this.contextMenuState.items, objects: this.contextMenuState.objects, style: {
3211
3216
  position: 'absolute',
3212
3217
  left: `${this.contextMenuState.position.x}px`,
3213
3218
  top: `${this.contextMenuState.position.y}px`,
3214
3219
  zIndex: '10002',
3215
- }, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), h("kritzel-sync-indicator", { key: '0094a026828473988f857d82cfccb811f36f049c', visible: this.isSyncing, text: "Syncing..." }), h("kritzel-loading-overlay", { key: '680096e20b6175104a866b9a92ca4c1d0087c93a', visible: this.isLoadingOverlayVisible, text: this.resolvedTerms['editor.loading'] ?? 'Loading...' }), h("kritzel-engine", { key: '0698a44fcbf2d6ea786d53502350ac0c9ffb9d66', ref: el => {
3220
+ }, onActionSelected: event => this.handleContextMenuActionSelected(event), onClose: () => this.hideContextMenu() })), h("kritzel-sync-indicator", { key: '9622af1634a939d6689d5a133496d536f5d7865f', visible: this.isSyncing, text: "Syncing..." }), h("kritzel-loading-overlay", { key: '00f142a3eb8b23fbd0bdb6ec53ae95855434516c', visible: this.isLoadingOverlayVisible, text: this.resolvedTerms['editor.loading'] ?? 'Loading...' }), h("kritzel-engine", { key: '39b1d7bfce0a5347fec60cd135d6db8fb4250bfa', ref: el => {
3216
3221
  if (el) {
3217
3222
  this.engineRef = el;
3218
3223
  }
3219
- }, 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) }), h("kritzel-controls", { key: '765a670807111ad68e9f8d14bdca2651a33704d4', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
3224
+ }, 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) }), h("kritzel-controls", { key: '9a708f21ab92ecc6953a35f5db8001b2403f330e', visible: this.isControlsVisible, class: { 'keyboard-open': this.isVirtualKeyboardOpen }, ref: el => {
3220
3225
  if (el) {
3221
3226
  this.controlsRef = el;
3222
3227
  }
3223
- }, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), h("div", { key: 'e7c5ebab4a40b7ab4fa5d19d203129326c0009ed', class: "bottom-left-buttons" }, h("kritzel-zoom-panel", { key: '0aa3891b553d2abcd06e635227fd6bb6656ed739', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), h("div", { key: '824244bbd78784c1d78ab89cc12e206ca61d84cc', class: "top-right-buttons" }, h("kritzel-settings", { key: 'baf4830b6b9ccb6241b1a1baa5cc6ff9d0295fb9', ref: el => {
3228
+ }, controls: this.normalizedControls, isUtilityPanelVisible: this.isUtilityPanelVisible, undoState: this.undoState ?? undefined, theme: this.theme, terms: this.resolvedTerms, onIsControlsReady: () => (this.isControlsReady = true) }), h("div", { key: '6a2b1cd72099db86952ac31aad9655693d0a6a0d', class: "bottom-left-buttons" }, h("kritzel-zoom-panel", { key: 'b3756514bf8de6de8ac7586c626f9759da7f231d', visible: this.isZoomPanelVisible, disabled: !this.isZoomingEnabled, zoomPercent: this.currentZoomPercent, terms: this.resolvedTerms, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut() })), h("div", { key: 'd5e118bc4d60b867006bac287c5516baf3aa890e', class: "top-right-buttons" }, h("kritzel-settings", { key: 'e4065d36beb9283cdbbf3ae9800f4f8011c0b4f0', ref: el => {
3224
3229
  if (el) {
3225
3230
  this.settingsRef = el;
3226
3231
  }
3227
- }, 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) }), h("kritzel-export", { key: '57e2682450c71803cd49bfe65f16e02d9d3358bc', ref: el => {
3232
+ }, 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) }), h("kritzel-export", { key: '4e18d9b42c9e7fef9433577b983c508559461da2', ref: el => {
3228
3233
  if (el) {
3229
3234
  this.exportRef = el;
3230
3235
  }
3231
- }, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), h("kritzel-active-users", { key: '955d2a61b98d937f8eec39ab19f1b73e702c9fd7', users: this.activeUsers }), shouldShowCurrentUser && h("kritzel-current-user", { key: 'c0885e0c8cab1a4cb50c8f6d4b17e03f32cc2b87', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (h("kritzel-current-user-dialog", { key: 'fc70555061d2aeaebdbe981bdbe8cf501bd8120e', ref: el => {
3236
+ }, workspaceName: this.activeWorkspace?.name || 'workspace', terms: this.resolvedTerms, onExportPng: () => this.engineRef.exportViewportAsPng(), onExportSvg: () => this.engineRef.exportViewportAsSvg(), onExportJson: event => this.engineRef.downloadAsJson(event.detail) }), h("kritzel-active-users", { key: '56e6b29c9c3dcd7b68af4a1ef7d34224d592ce34', users: this.activeUsers }), shouldShowCurrentUser && h("kritzel-current-user", { key: '3b90bcc7e583fcf3c6845d222ad181ccf234a423', user: this.user, terms: this.resolvedTerms, onClick: () => this.currentUserDialogRef?.open() }), shouldShowCurrentUser && (h("kritzel-current-user-dialog", { key: 'f87702c5a708b6afdb2bc3760895e3e3650a4cb5', ref: el => {
3232
3237
  if (el) {
3233
3238
  this.currentUserDialogRef = el;
3234
3239
  }
3235
- }, user: this.user, terms: this.resolvedTerms, onLogoutRequest: this.handleCurrentUserLogout })), shouldShowLoginButton && h("kritzel-button", { key: '9666163a2a36a64ecb641659dca515ae30409d0f', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in'), h("kritzel-more-menu", { key: '167ac17ea93897597e83d4fe8b3b561fac848bb6', items: this.resolvedMoreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), h("kritzel-share-dialog", { key: '10cfb0b794d613ec39299f06ac1a3402d1ad3084', ref: el => {
3240
+ }, user: this.user, terms: this.resolvedTerms, onLogoutRequest: this.handleCurrentUserLogout })), shouldShowLoginButton && h("kritzel-button", { key: '015bc0f4c66352136de66d35d28c20070366933f', onButtonClick: () => this.loginDialogRef?.open() }, this.resolvedTerms['login.dialogTitle'] ?? 'Sign in'), h("kritzel-more-menu", { key: 'b3fdf9fdb2a34508002e82dfdef9e9bdbbe57ecc', items: this.resolvedMoreMenuItems, visible: this.isMoreMenuVisible, terms: this.resolvedTerms }), h("kritzel-share-dialog", { key: 'bac4fbbfa04c261e2b128e766cc43c1062aec2ff', ref: el => {
3236
3241
  if (el) {
3237
3242
  this.shareDialogRef = el;
3238
3243
  }
3239
- }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (h("kritzel-login-dialog", { key: '99dcb9051382982ec5a8975bdd991bae57745bb9', ref: el => {
3244
+ }, isPublic: this.currentIsPublic, workspaceId: this.activeWorkspace?.id, terms: this.resolvedTerms, onToggleIsPublic: this.handleToggleIsPublic }), this.loginConfig && (h("kritzel-login-dialog", { key: 'a7d52bad9e752ad546adf80d257c6a46de031e76', ref: el => {
3240
3245
  if (el) {
3241
3246
  this.loginDialogRef = el;
3242
3247
  }
@@ -22279,6 +22284,10 @@ class KritzelObjectMap {
22279
22284
  _awarenessChangeHandler = null;
22280
22285
  _awarenessChangeCallbacks = [];
22281
22286
  _objectsChangeCallbacks = [];
22287
+ _remoteChangesCallbacks = [];
22288
+ _docUpdateHandler = null;
22289
+ _remoteOrigins = new Set();
22290
+ _appliedRemoteChanges = false;
22282
22291
  _lastAwarenessEmitTime = 0;
22283
22292
  _awarenessEmitTimeout = null;
22284
22293
  AWARENESS_THROTTLE_INTERVAL = 100; // milliseconds
@@ -22302,12 +22311,44 @@ class KritzelObjectMap {
22302
22311
  return this._isReady;
22303
22312
  }
22304
22313
  /**
22305
- * Promise that settles when network providers finish connecting.
22306
- * Resolves immediately if no network providers. Awaited by the loading overlay.
22314
+ * Promise that settles when network providers finish connecting and complete
22315
+ * their Yjs sync handshake. Resolves immediately if no network providers.
22307
22316
  */
22308
22317
  whenNetworkSynced() {
22309
22318
  return this._networkSyncPromise ?? Promise.resolve();
22310
22319
  }
22320
+ /**
22321
+ * Whether any configured provider synchronizes over the network.
22322
+ */
22323
+ get hasNetworkSyncProvider() {
22324
+ return this._providers.some(provider => provider.type === 'network');
22325
+ }
22326
+ /**
22327
+ * Whether remote-originated changes can be told apart from local writes.
22328
+ * Requires every network provider to expose its Yjs update origin.
22329
+ */
22330
+ get supportsRemoteChangeDetection() {
22331
+ const networkProviders = this._providers.filter(provider => provider.type === 'network');
22332
+ return networkProviders.length > 0 && networkProviders.every(provider => provider.updateOrigin !== undefined);
22333
+ }
22334
+ /**
22335
+ * Whether at least one update originating from a network provider has been
22336
+ * applied to this document. Stays `false` when the remote held no data the
22337
+ * local document was missing, because Yjs suppresses empty updates.
22338
+ */
22339
+ get hasAppliedRemoteChanges() {
22340
+ return this._appliedRemoteChanges;
22341
+ }
22342
+ /**
22343
+ * Registers a callback invoked whenever a network provider applies changes to this document.
22344
+ * @returns A function that unregisters the callback.
22345
+ */
22346
+ onRemoteChangesApplied(callback) {
22347
+ this._remoteChangesCallbacks.push(callback);
22348
+ return () => {
22349
+ this._remoteChangesCallbacks = this._remoteChangesCallbacks.filter(registered => registered !== callback);
22350
+ };
22351
+ }
22311
22352
  /**
22312
22353
  * Returns the Yjs Awareness instance, if a network provider is available.
22313
22354
  */
@@ -22550,6 +22591,22 @@ class KritzelObjectMap {
22550
22591
  const providerList = this._providers.map(p => `${p.constructor.name} (${p.type})`).join(', ');
22551
22592
  console.info(`[Kritzel] Workspace sync providers initialized for ${docName}: ${providerList || 'none'}`);
22552
22593
  }
22594
+ // Registered before any provider connects so no remote update can be missed.
22595
+ for (const provider of this._providers) {
22596
+ if (provider.type === 'network' && provider.updateOrigin !== undefined) {
22597
+ this._remoteOrigins.add(provider.updateOrigin);
22598
+ }
22599
+ }
22600
+ this._docUpdateHandler = (_update, origin) => {
22601
+ if (!this._remoteOrigins.has(origin)) {
22602
+ return;
22603
+ }
22604
+ this._appliedRemoteChanges = true;
22605
+ for (const callback of [...this._remoteChangesCallbacks]) {
22606
+ callback();
22607
+ }
22608
+ };
22609
+ this._ydoc.on('update', this._docUpdateHandler);
22553
22610
  // captureTimeout is effectively infinite — undo-step boundaries are
22554
22611
  // marked explicitly via markUndoBoundary() in insert/remove/reset and
22555
22612
  // at stroke/gesture boundaries in the tools. This prevents a single
@@ -22603,12 +22660,15 @@ class KritzelObjectMap {
22603
22660
  if (showSyncProviderInfo && networkProviders.length > 0) {
22604
22661
  console.info(`[Kritzel] Workspace network providers connecting in background: ${networkProviders.length}`);
22605
22662
  }
22606
- const networkConnectPromises = networkProviders.map(provider => provider.connect().catch(err => {
22663
+ const networkConnectPromises = networkProviders.map(provider => provider
22664
+ .connect()
22665
+ .then(() => provider.whenSynced?.())
22666
+ .catch(err => {
22607
22667
  if (showSyncProviderInfo) {
22608
22668
  console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
22609
22669
  }
22610
22670
  }));
22611
- // Network sync awaited by loading overlay to show remote workspace hydration
22671
+ // Settles once remote data has been reconciled, not merely once the transport connected
22612
22672
  this._networkSyncPromise = networkConnectPromises.length > 0 ? Promise.allSettled(networkConnectPromises).then(() => undefined) : null;
22613
22673
  this._isReady = true;
22614
22674
  // Find the first provider that exposes awareness (network providers)
@@ -23266,6 +23326,13 @@ class KritzelObjectMap {
23266
23326
  this._objectsMap.unobserve(this._objectsObserver);
23267
23327
  this._objectsObserver = null;
23268
23328
  }
23329
+ if (this._ydoc && this._docUpdateHandler) {
23330
+ this._ydoc.off('update', this._docUpdateHandler);
23331
+ }
23332
+ this._docUpdateHandler = null;
23333
+ this._remoteOrigins.clear();
23334
+ this._remoteChangesCallbacks = [];
23335
+ this._appliedRemoteChanges = false;
23269
23336
  // Remove undo manager event listeners
23270
23337
  if (this._undoManager) {
23271
23338
  if (this._stackItemAddedHandler) {
@@ -23761,7 +23828,10 @@ class KritzelAppStateMap {
23761
23828
  if (showSyncProviderInfo && networkProviders.length > 0) {
23762
23829
  console.info(`[Kritzel] App-state network providers connecting in background: ${networkProviders.length}`);
23763
23830
  }
23764
- const networkConnectPromises = networkProviders.map(provider => provider.connect().catch(err => {
23831
+ const networkConnectPromises = networkProviders.map(provider => provider
23832
+ .connect()
23833
+ .then(() => provider.whenSynced?.())
23834
+ .catch(err => {
23765
23835
  if (showSyncProviderInfo) {
23766
23836
  console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
23767
23837
  }
@@ -27865,7 +27935,7 @@ const KritzelEngine = class {
27865
27935
  contextMenuStateChange;
27866
27936
  /** Emitted when the combined loading state (external `isLoading` prop or internal workspace loading) changes. */
27867
27937
  loadingChange;
27868
- /** Emitted when remote workspace synchronization is in progress. */
27938
+ /** Emitted while remote changes are being applied to the active workspace. Stays silent when the remote holds nothing new. */
27869
27939
  syncingChange;
27870
27940
  forceUpdate = 0;
27871
27941
  isSyncing = false;
@@ -29452,6 +29522,7 @@ const KritzelEngine = class {
29452
29522
  _sceneBootstrapOpacity = 1;
29453
29523
  _sceneBootstrapTransitionEnabled = false;
29454
29524
  _sceneBootstrapTransitionTimer = null;
29525
+ _remoteChangesUnsubscribe = null;
29455
29526
  _defaultUndoState = {
29456
29527
  canUndo: false,
29457
29528
  canRedo: false,
@@ -29469,9 +29540,16 @@ const KritzelEngine = class {
29469
29540
  this.loadingChange.emit(this.core.store.state.isLoading);
29470
29541
  }
29471
29542
  syncSyncingState(isSyncing) {
29543
+ if (this.isSyncing === isSyncing) {
29544
+ return;
29545
+ }
29472
29546
  this.isSyncing = isSyncing;
29473
29547
  this.syncingChange.emit(isSyncing);
29474
29548
  }
29549
+ clearRemoteChangesSubscription() {
29550
+ this._remoteChangesUnsubscribe?.();
29551
+ this._remoteChangesUnsubscribe = null;
29552
+ }
29475
29553
  isNetworkAvailable() {
29476
29554
  return typeof navigator !== 'undefined' && navigator.onLine;
29477
29555
  }
@@ -29583,6 +29661,7 @@ const KritzelEngine = class {
29583
29661
  disconnectedCallback() {
29584
29662
  this.throttledPointerMoveMulti.cancel();
29585
29663
  this._revealedImageKeys.clear();
29664
+ this.clearRemoteChangesSubscription();
29586
29665
  if (this._sceneBootstrapTransitionTimer !== null) {
29587
29666
  clearTimeout(this._sceneBootstrapTransitionTimer);
29588
29667
  this._sceneBootstrapTransitionTimer = null;
@@ -29837,21 +29916,7 @@ const KritzelEngine = class {
29837
29916
  this._workspaceInitializationTargetKey = targetKey;
29838
29917
  try {
29839
29918
  await initializationPromise;
29840
- // Track remote sync in the background without blocking the loading overlay.
29841
- const initializedObjects = this.core.store.objects;
29842
- const networkSyncPromise = initializedObjects?.whenNetworkSynced();
29843
- if (networkSyncPromise && this.isNetworkAvailable()) {
29844
- this.syncSyncingState(true);
29845
- void networkSyncPromise.finally(() => {
29846
- // Only clear syncing for the currently active object map.
29847
- if (this.core.store.objects === initializedObjects) {
29848
- this.syncSyncingState(false);
29849
- }
29850
- });
29851
- }
29852
- else {
29853
- this.syncSyncingState(false);
29854
- }
29919
+ this.trackRemoteSynchronization();
29855
29920
  }
29856
29921
  finally {
29857
29922
  // Only the most recent init clears loading state to prevent race conditions
@@ -29863,6 +29928,38 @@ const KritzelEngine = class {
29863
29928
  }
29864
29929
  }
29865
29930
  }
29931
+ /**
29932
+ * Reports syncing only while remote data is actually being reconciled into the
29933
+ * active workspace. Yjs suppresses updates that carry nothing new, so a workspace
29934
+ * that already matches the remote never reports syncing at all.
29935
+ */
29936
+ trackRemoteSynchronization() {
29937
+ this.clearRemoteChangesSubscription();
29938
+ this.syncSyncingState(false);
29939
+ const initializedObjects = this.core.store.objects;
29940
+ if (!initializedObjects?.hasNetworkSyncProvider || !this.isNetworkAvailable()) {
29941
+ return;
29942
+ }
29943
+ if (initializedObjects.supportsRemoteChangeDetection) {
29944
+ this._remoteChangesUnsubscribe = initializedObjects.onRemoteChangesApplied(() => {
29945
+ if (this.core.store.objects !== initializedObjects) {
29946
+ return;
29947
+ }
29948
+ this.syncSyncingState(true);
29949
+ });
29950
+ }
29951
+ else {
29952
+ // Custom providers without an update origin cannot be diffed, so fall back to reporting the whole handshake.
29953
+ this.syncSyncingState(true);
29954
+ }
29955
+ void initializedObjects.whenNetworkSynced().finally(() => {
29956
+ if (this.core.store.objects !== initializedObjects) {
29957
+ return;
29958
+ }
29959
+ this.clearRemoteChangesSubscription();
29960
+ this.syncSyncingState(false);
29961
+ });
29962
+ }
29866
29963
  emitObjectsChange() {
29867
29964
  const objectsMap = this.core.store.objects;
29868
29965
  if (!objectsMap) {
@@ -32007,7 +32104,7 @@ const KritzelPortal = class {
32007
32104
  * This file is auto-generated by the version bump scripts.
32008
32105
  * Do not modify manually.
32009
32106
  */
32010
- const KRITZEL_VERSION = '0.4.21';
32107
+ const KRITZEL_VERSION = '0.4.22';
32011
32108
 
32012
32109
  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)}`;
32013
32110
 
@@ -549,9 +549,15 @@ class WebSocketSyncProvider {
549
549
  provider;
550
550
  isConnected = false;
551
551
  _quiet = false;
552
+ _isSynced = false;
553
+ _syncedResolvers = [];
552
554
  get awareness() {
553
555
  return this.provider.awareness;
554
556
  }
557
+ /** y-websocket applies remote updates with the underlying provider as the Yjs transaction origin. */
558
+ get updateOrigin() {
559
+ return this.provider;
560
+ }
555
561
  constructor(docName, doc, options) {
556
562
  const url = options?.url || 'ws://localhost:1234';
557
563
  const roomName = options?.roomName || docName;
@@ -598,11 +604,29 @@ class WebSocketSyncProvider {
598
604
  }
599
605
  });
600
606
  this.provider.on('sync', (synced) => {
601
- if (synced && !this._quiet) {
607
+ if (!synced) {
608
+ return;
609
+ }
610
+ this._isSynced = true;
611
+ this.releaseSyncedWaiters();
612
+ if (!this._quiet) {
602
613
  console.info('WebSocket synced');
603
614
  }
604
615
  });
605
616
  }
617
+ releaseSyncedWaiters() {
618
+ const resolvers = this._syncedResolvers;
619
+ this._syncedResolvers = [];
620
+ resolvers.forEach(resolve => resolve());
621
+ }
622
+ async whenSynced() {
623
+ if (this._isSynced || this.provider.synced) {
624
+ return;
625
+ }
626
+ return new Promise(resolve => {
627
+ this._syncedResolvers.push(resolve);
628
+ });
629
+ }
606
630
  async connect() {
607
631
  if (this.isConnected) {
608
632
  return;
@@ -634,6 +658,8 @@ class WebSocketSyncProvider {
634
658
  this.provider.disconnect();
635
659
  }
636
660
  this.isConnected = false;
661
+ this._isSynced = false;
662
+ this.releaseSyncedWaiters();
637
663
  }
638
664
  async reconnect() {
639
665
  this.disconnect();
@@ -644,6 +670,8 @@ class WebSocketSyncProvider {
644
670
  this.provider.destroy();
645
671
  }
646
672
  this.isConnected = false;
673
+ this._isSynced = false;
674
+ this.releaseSyncedWaiters();
647
675
  }
648
676
  }
649
677
 
@@ -665,9 +693,14 @@ class HocuspocusSyncProvider {
665
693
  _connectionStatus = 'disconnected';
666
694
  visibilityHandler = null;
667
695
  onlineHandler = null;
696
+ syncedResolvers = [];
668
697
  get awareness() {
669
698
  return this.provider.awareness;
670
699
  }
700
+ /** HocuspocusProvider applies remote updates with itself as the Yjs transaction origin. */
701
+ get updateOrigin() {
702
+ return this.provider;
703
+ }
671
704
  get connectionStatus() {
672
705
  return this._connectionStatus;
673
706
  }
@@ -724,6 +757,7 @@ class HocuspocusSyncProvider {
724
757
  }
725
758
  this.isSynced = true;
726
759
  this._connectionStatus = 'synced';
760
+ this.releaseSyncedWaiters();
727
761
  if (!options?.quiet) {
728
762
  console.info(`Hocuspocus synced: ${name}`);
729
763
  }
@@ -934,6 +968,19 @@ class HocuspocusSyncProvider {
934
968
  this.disconnect();
935
969
  return this.connect();
936
970
  }
971
+ releaseSyncedWaiters() {
972
+ const resolvers = this.syncedResolvers;
973
+ this.syncedResolvers = [];
974
+ resolvers.forEach(resolve => resolve());
975
+ }
976
+ async whenSynced() {
977
+ if (this.isSynced || this.isDestroyed || this.provider.isSynced) {
978
+ return;
979
+ }
980
+ return new Promise(resolve => {
981
+ this.syncedResolvers.push(resolve);
982
+ });
983
+ }
937
984
  disconnect() {
938
985
  // Cancel any pending connection attempt
939
986
  if (this.connectTimeout) {
@@ -955,6 +1002,7 @@ class HocuspocusSyncProvider {
955
1002
  this.isConnected = false;
956
1003
  this.isSynced = false;
957
1004
  this._connectionStatus = 'disconnected';
1005
+ this.releaseSyncedWaiters();
958
1006
  }
959
1007
  destroy() {
960
1008
  // Mark as destroyed first to prevent any callbacks from doing work
@@ -974,6 +1022,7 @@ class HocuspocusSyncProvider {
974
1022
  this.isConnected = false;
975
1023
  this.isSynced = false;
976
1024
  this._connectionStatus = 'disconnected';
1025
+ this.releaseSyncedWaiters();
977
1026
  }
978
1027
  }
979
1028