kritzel-stencil 0.1.92 → 0.1.94

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 (68) hide show
  1. package/dist/cjs/index.cjs.js +1 -1
  2. package/dist/cjs/kritzel-active-users_42.cjs.entry.js +93 -11
  3. package/dist/cjs/{workspace.migrations-PaftqSjk.js → workspace.migrations-BENHTbRC.js} +101 -9
  4. package/dist/collection/classes/core/core.class.js +15 -3
  5. package/dist/collection/classes/objects/base-object.class.js +14 -0
  6. package/dist/collection/classes/objects/image.class.js +86 -9
  7. package/dist/collection/classes/registries/icon-registry.class.js +1 -0
  8. package/dist/collection/classes/structures/object-map.structure.js +8 -0
  9. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +68 -6
  10. package/dist/collection/constants/version.js +1 -1
  11. package/dist/components/index.js +1 -1
  12. package/dist/components/kritzel-awareness-cursors.js +1 -1
  13. package/dist/components/kritzel-back-to-content.js +1 -1
  14. package/dist/components/kritzel-brush-style.js +1 -1
  15. package/dist/components/kritzel-context-menu.js +1 -1
  16. package/dist/components/kritzel-controls.js +1 -1
  17. package/dist/components/kritzel-editor.js +1 -1
  18. package/dist/components/kritzel-engine.js +1 -1
  19. package/dist/components/kritzel-export.js +1 -1
  20. package/dist/components/kritzel-icon.js +1 -1
  21. package/dist/components/kritzel-login-dialog.js +1 -1
  22. package/dist/components/kritzel-master-detail.js +1 -1
  23. package/dist/components/kritzel-menu-item.js +1 -1
  24. package/dist/components/kritzel-menu.js +1 -1
  25. package/dist/components/kritzel-more-menu.js +1 -1
  26. package/dist/components/kritzel-pill-tabs.js +1 -1
  27. package/dist/components/kritzel-settings.js +1 -1
  28. package/dist/components/kritzel-share-dialog.js +1 -1
  29. package/dist/components/kritzel-split-button.js +1 -1
  30. package/dist/components/kritzel-tool-config.js +1 -1
  31. package/dist/components/kritzel-utility-panel.js +1 -1
  32. package/dist/components/kritzel-workspace-manager.js +1 -1
  33. package/dist/components/p-A7Ult9iv.js +1 -0
  34. package/dist/components/p-B2kHVHa_.js +1 -0
  35. package/dist/components/{p-CRdrQOlL.js → p-BFQVg_eQ.js} +1 -1
  36. package/dist/components/{p-CrCtvLMx.js → p-BoRQF_Zc.js} +1 -1
  37. package/dist/components/{p-DRbR0Li3.js → p-BvgGpgKP.js} +1 -1
  38. package/dist/components/{p-DmNh83AY.js → p-Bx8daVwR.js} +2 -2
  39. package/dist/components/{p-BMPgR5Bt.js → p-CFzvz-B2.js} +1 -1
  40. package/dist/components/{p-CKdGsPx9.js → p-CK29qhZR.js} +1 -1
  41. package/dist/components/{p-D8fQwcNC.js → p-CU6kJPth.js} +1 -1
  42. package/dist/components/{p-CdvApfJt.js → p-CY9ooSqo.js} +1 -1
  43. package/dist/components/{p-7yTPTHbQ.js → p-ChQNi67Z.js} +1 -1
  44. package/dist/components/{p-DXdAYm-y.js → p-ChqeIKg_.js} +1 -1
  45. package/dist/components/{p-DPmAV68B.js → p-CoyqJSjT.js} +1 -1
  46. package/dist/components/{p-CowdEK08.js → p-CqYIRmoh.js} +1 -1
  47. package/dist/components/{p-cVQef3Hq.js → p-Cra28iyu.js} +1 -1
  48. package/dist/components/{p-CBq-KE9C.js → p-Czaea0WP.js} +1 -1
  49. package/dist/components/{p-TIoiUjzO.js → p-DACQ8HHJ.js} +1 -1
  50. package/dist/components/{p-DMvIGnOt.js → p-DVEfOb8T.js} +1 -1
  51. package/dist/components/{p-DBZyCAsW.js → p-DkT0CXfN.js} +1 -1
  52. package/dist/components/{p-BTguWTDZ.js → p-mYhFNPgz.js} +1 -1
  53. package/dist/esm/index.js +2 -2
  54. package/dist/esm/kritzel-active-users_42.entry.js +93 -11
  55. package/dist/esm/{workspace.migrations-Cz5xML0x.js → workspace.migrations-CfJnWHNg.js} +101 -9
  56. package/dist/stencil/index.esm.js +1 -1
  57. package/dist/stencil/{p-5498d2e1.entry.js → p-5fdd1dea.entry.js} +2 -2
  58. package/dist/stencil/p-CfJnWHNg.js +1 -0
  59. package/dist/stencil/stencil.esm.js +1 -1
  60. package/dist/types/classes/core/core.class.d.ts +13 -2
  61. package/dist/types/classes/objects/base-object.class.d.ts +12 -0
  62. package/dist/types/classes/objects/image.class.d.ts +40 -0
  63. package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +9 -1
  64. package/dist/types/constants/version.d.ts +1 -1
  65. package/package.json +1 -1
  66. package/dist/components/p-DQK_4lkI.js +0 -1
  67. package/dist/components/p-Gm5hSQ-e.js +0 -1
  68. package/dist/stencil/p-Cz5xML0x.js +0 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var workspace_migrations = require('./workspace.migrations-PaftqSjk.js');
3
+ var workspace_migrations = require('./workspace.migrations-BENHTbRC.js');
4
4
  var Y = require('yjs');
5
5
  var yWebsocket = require('y-websocket');
6
6
  require('y-indexeddb');
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var index = require('./index-CFnj_FXt.js');
4
- var workspace_migrations = require('./workspace.migrations-PaftqSjk.js');
4
+ var workspace_migrations = require('./workspace.migrations-BENHTbRC.js');
5
5
  var Y = require('yjs');
6
6
  require('y-indexeddb');
7
7
  require('y-websocket');
@@ -21767,6 +21767,10 @@ class KritzelObjectMap {
21767
21767
  objectsToUpdate.forEach(object => {
21768
21768
  const existed = this._idMap.has(object.id);
21769
21769
  if (existed) {
21770
+ const previous = this._idMap.get(object.id);
21771
+ if (previous && typeof object.adoptTransientStateFrom === 'function') {
21772
+ object.adoptTransientStateFrom(previous);
21773
+ }
21770
21774
  this.quadtree.update(object);
21771
21775
  this._idMap.set(object.id, object);
21772
21776
  updatedObjects.push(object);
@@ -21782,6 +21786,10 @@ class KritzelObjectMap {
21782
21786
  selectionGroupsToUpdate.forEach(object => {
21783
21787
  const existed = this._idMap.has(object.id);
21784
21788
  if (existed) {
21789
+ const previous = this._idMap.get(object.id);
21790
+ if (previous && typeof object.adoptTransientStateFrom === 'function') {
21791
+ object.adoptTransientStateFrom(previous);
21792
+ }
21785
21793
  this.quadtree.update(object);
21786
21794
  this._idMap.set(object.id, object);
21787
21795
  }
@@ -23324,12 +23332,24 @@ class KritzelCore {
23324
23332
  }
23325
23333
  /**
23326
23334
  * Initializes the Yjs document for collaborative editing.
23327
- * Sets up the app state map with the current sync configuration and
23328
- * initializes the asset storage layer.
23335
+ * Sets up the app state map with the current sync configuration.
23336
+ *
23337
+ * Asset storage is initialized separately via {@link initializeAssetStorage}
23338
+ * so that it can be deferred until the `assetStorageConfig` prop arrives,
23339
+ * mirroring the lazy-init pattern used for sync config.
23329
23340
  */
23330
23341
  async initializeYjs() {
23331
- await this._assetResolver.init(this._assetStorageConfig);
23332
23342
  await this._appStateMap.initialize(this, this._syncConfig);
23343
+ }
23344
+ /**
23345
+ * Initializes the asset storage layer with the current asset storage
23346
+ * configuration. Safe to call multiple times: subsequent calls are
23347
+ * no-ops because {@link KritzelAssetResolver.init} is idempotent.
23348
+ * To apply a new configuration after initialization, destroy the
23349
+ * resolver first or use {@link KritzelEngine.reinitSync}.
23350
+ */
23351
+ async initializeAssetStorage() {
23352
+ await this._assetResolver.init(this._assetStorageConfig);
23333
23353
  this.warnIfAssetStorageMismatched();
23334
23354
  }
23335
23355
  /**
@@ -25731,11 +25751,18 @@ const KritzelEngine = class {
25731
25751
  * such as `HttpAssetProvider` or `PresignedAssetProvider`.
25732
25752
  */
25733
25753
  assetStorageConfig;
25734
- onAssetStorageConfigChange(newValue) {
25735
- // Forward to core so initializeYjs picks up the latest value. The
25736
- // resolver itself is only initialized once per engine; swapping
25737
- // providers at runtime requires reinitSync().
25754
+ async onAssetStorageConfigChange(newValue) {
25755
+ this._assetStorageConfigRevision++;
25756
+ // Keep core config in sync immediately so late prop updates are not lost.
25738
25757
  this.core.setAssetStorageConfig(newValue);
25758
+ // If assetStorageConfig arrives after componentDidLoad, initialize the
25759
+ // asset storage layer now. Mirrors the syncConfig watcher: the resolver
25760
+ // is only initialized once, when a non-undefined config first becomes
25761
+ // available. Subsequent changes after initialization require
25762
+ // `reinitSync()`, identical to syncConfig semantics.
25763
+ if (newValue && !this._isAssetStorageInitialized && this._isViewportReady) {
25764
+ await this.initializeAssetStorage();
25765
+ }
25739
25766
  }
25740
25767
  /** The current user for awareness broadcasting (name, id, cursor position). */
25741
25768
  user;
@@ -26849,7 +26876,9 @@ const KritzelEngine = class {
26849
26876
  this.core.store.objects?.clearCursorPosition();
26850
26877
  this.core.store.objects?.destroy();
26851
26878
  this.core.appStateMap.destroy();
26879
+ this.core.assetResolver.destroy();
26852
26880
  this._isYjsInitialized = false;
26881
+ this._isAssetStorageInitialized = false;
26853
26882
  await this.initializeSyncAndWorkspace();
26854
26883
  }
26855
26884
  core;
@@ -26860,12 +26889,14 @@ const KritzelEngine = class {
26860
26889
  _lastHadSelectionGroup = false;
26861
26890
  _isViewportReady = false;
26862
26891
  _isYjsInitialized = false;
26892
+ _isAssetStorageInitialized = false;
26863
26893
  _isResolvingActiveWorkspaceId = false;
26864
26894
  _stateChangeListenersRegistered = false;
26865
26895
  _workspaceInitializationPromise = null;
26866
26896
  _workspaceInitializationTargetKey = null;
26867
26897
  _syncInitPromise = null;
26868
26898
  _syncConfigRevision = 0;
26899
+ _assetStorageConfigRevision = 0;
26869
26900
  _isWorkspaceLoading = false;
26870
26901
  _defaultUndoState = {
26871
26902
  canUndo: false,
@@ -26963,6 +26994,27 @@ const KritzelEngine = class {
26963
26994
  this._syncInitPromise = null;
26964
26995
  }
26965
26996
  }
26997
+ /**
26998
+ * Initializes the asset storage layer with the latest config. Mirrors
26999
+ * the in-flight replay logic of {@link doInitializeSyncAndWorkspace} for
27000
+ * `assetStorageConfig` prop updates that arrive while init is running.
27001
+ */
27002
+ async initializeAssetStorage() {
27003
+ if (this._isAssetStorageInitialized) {
27004
+ return;
27005
+ }
27006
+ const revisionAtStart = this._assetStorageConfigRevision;
27007
+ this.core.setAssetStorageConfig(this.assetStorageConfig);
27008
+ await this.core.initializeAssetStorage();
27009
+ // If assetStorageConfig changed mid-flight, replay once with the latest
27010
+ // value so late prop updates are not lost.
27011
+ if (revisionAtStart !== this._assetStorageConfigRevision) {
27012
+ this.core.assetResolver.destroy();
27013
+ this.core.setAssetStorageConfig(this.assetStorageConfig);
27014
+ await this.core.initializeAssetStorage();
27015
+ }
27016
+ this._isAssetStorageInitialized = true;
27017
+ }
26966
27018
  async doInitializeSyncAndWorkspace() {
26967
27019
  // Capture sync config revision to detect prop updates that happen while
26968
27020
  // initialization is in-flight.
@@ -26983,6 +27035,12 @@ const KritzelEngine = class {
26983
27035
  }
26984
27036
  this._isYjsInitialized = true;
26985
27037
  }
27038
+ // Initialize the asset storage layer if a config has been provided.
27039
+ // When the prop arrives later, the @Watch('assetStorageConfig') handler
27040
+ // will trigger initializeAssetStorage() instead.
27041
+ if (!this._isAssetStorageInitialized && this.assetStorageConfig) {
27042
+ await this.initializeAssetStorage();
27043
+ }
26986
27044
  if (this.activeWorkspaceId) {
26987
27045
  const startupWorkspace = this.core.getWorkspaces().find(ws => ws.id === this.activeWorkspaceId);
26988
27046
  if (startupWorkspace) {
@@ -27187,7 +27245,7 @@ const KritzelEngine = class {
27187
27245
  opacity: object.markedForRemoval ? '0.5' : object.opacity.toString(),
27188
27246
  pointerEvents: object.markedForRemoval ? 'none' : 'auto',
27189
27247
  overflow: 'visible',
27190
- }, viewBox: object?.viewBox }, (object.hasStartArrow || object.hasEndArrow) && (index.h("defs", null, object.hasStartArrow && (index.h("marker", { id: object.startMarkerId, markerWidth: object.getArrowSize('start'), markerHeight: object.getArrowSize('start'), refX: 0, refY: object.getArrowSize('start') / 2, orient: "auto-start-reverse", markerUnits: "userSpaceOnUse" }, index.h("path", { d: object.getArrowPath(object.arrows?.start?.style), fill: object.getArrowFill('start'), transform: `scale(${object.getArrowSize('start') / 10})` }))), object.hasEndArrow && (index.h("marker", { id: object.endMarkerId, markerWidth: object.getArrowSize('end'), markerHeight: object.getArrowSize('end'), refX: 0, refY: object.getArrowSize('end') / 2, orient: "auto", markerUnits: "userSpaceOnUse" }, index.h("path", { d: object.getArrowPath(object.arrows?.end?.style), fill: object.getArrowFill('end'), transform: `scale(${object.getArrowSize('end') / 10})` }))))), index.h("path", { d: this.core.anchorManager.computeClippedLinePath(object), fill: "none", stroke: "transparent", "stroke-width": Math.max(object?.strokeWidth || 0, 10), "stroke-linecap": "round" }), index.h("path", { d: this.core.anchorManager.computeClippedLinePath(object), fill: "none", stroke: workspace_migrations.KritzelColorHelper.resolveThemeColor(object?.stroke, currentTheme), "stroke-width": object?.strokeWidth, "stroke-linecap": "round", "marker-start": object.hasStartArrow ? `url(#${object.startMarkerId})` : undefined, "marker-end": object.hasEndArrow ? `url(#${object.endMarkerId})` : undefined }))), workspace_migrations.KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && (index.h("img", { ref: el => el && object.mount(el), src: object.resolvedSrc || object.src, style: {
27248
+ }, viewBox: object?.viewBox }, (object.hasStartArrow || object.hasEndArrow) && (index.h("defs", null, object.hasStartArrow && (index.h("marker", { id: object.startMarkerId, markerWidth: object.getArrowSize('start'), markerHeight: object.getArrowSize('start'), refX: 0, refY: object.getArrowSize('start') / 2, orient: "auto-start-reverse", markerUnits: "userSpaceOnUse" }, index.h("path", { d: object.getArrowPath(object.arrows?.start?.style), fill: object.getArrowFill('start'), transform: `scale(${object.getArrowSize('start') / 10})` }))), object.hasEndArrow && (index.h("marker", { id: object.endMarkerId, markerWidth: object.getArrowSize('end'), markerHeight: object.getArrowSize('end'), refX: 0, refY: object.getArrowSize('end') / 2, orient: "auto", markerUnits: "userSpaceOnUse" }, index.h("path", { d: object.getArrowPath(object.arrows?.end?.style), fill: object.getArrowFill('end'), transform: `scale(${object.getArrowSize('end') / 10})` }))))), index.h("path", { d: this.core.anchorManager.computeClippedLinePath(object), fill: "none", stroke: "transparent", "stroke-width": Math.max(object?.strokeWidth || 0, 10), "stroke-linecap": "round" }), index.h("path", { d: this.core.anchorManager.computeClippedLinePath(object), fill: "none", stroke: workspace_migrations.KritzelColorHelper.resolveThemeColor(object?.stroke, currentTheme), "stroke-width": object?.strokeWidth, "stroke-linecap": "round", "marker-start": object.hasStartArrow ? `url(#${object.startMarkerId})` : undefined, "marker-end": object.hasEndArrow ? `url(#${object.endMarkerId})` : undefined }))), workspace_migrations.KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && object.loadState === 'ready' && (index.h("img", { ref: el => el && object.mount(el), src: object.resolvedSrc || object.src, style: {
27191
27249
  position: 'absolute',
27192
27250
  left: '0',
27193
27251
  top: '0',
@@ -27205,7 +27263,31 @@ const KritzelEngine = class {
27205
27263
  overflow: 'visible',
27206
27264
  userSelect: 'none',
27207
27265
  imageRendering: this.core.store.state.isScaling || this.core.store.state.isPanning ? 'pixelated' : 'auto',
27208
- }, draggable: false, onDragStart: e => e.preventDefault() })), workspace_migrations.KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement') && (index.h("div", { ref: el => el && object.mount(el), style: {
27266
+ }, draggable: false, onDragStart: e => e.preventDefault() })), workspace_migrations.KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && object.loadState !== 'ready' && (index.h("div", { ref: () => object.ensureLoaded(), style: {
27267
+ position: 'absolute',
27268
+ left: '0',
27269
+ top: '0',
27270
+ width: object.totalWidth + 'px',
27271
+ height: object.totalHeight + 'px',
27272
+ transform: object.rotationDegrees !== 0 ? `rotate(${object.rotationDegrees}deg)` : undefined,
27273
+ transformOrigin: object.rotationDegrees !== 0 ? `${object.totalWidth / 2}px ${object.totalHeight / 2}px` : undefined,
27274
+ opacity: object.markedForRemoval ? '0.5' : object.opacity.toString(),
27275
+ pointerEvents: object.markedForRemoval ? 'none' : 'auto',
27276
+ backgroundColor: workspace_migrations.KritzelColorHelper.resolveThemeColor({ light: '#e5e7eb', dark: '#2a2a2a' }, currentTheme),
27277
+ borderColor: object.loadState === 'error'
27278
+ ? workspace_migrations.KritzelColorHelper.resolveThemeColor({ light: '#9ca3af', dark: '#6b7280' }, currentTheme)
27279
+ : workspace_migrations.KritzelColorHelper.resolveThemeColor(object.borderColor, currentTheme),
27280
+ borderWidth: object.loadState === 'error' ? '1px' : object.borderWidth + 'px',
27281
+ borderStyle: 'solid',
27282
+ padding: object.padding + 'px',
27283
+ overflow: 'hidden',
27284
+ userSelect: 'none',
27285
+ display: 'flex',
27286
+ alignItems: 'center',
27287
+ justifyContent: 'center',
27288
+ } }, index.h("kritzel-icon", { name: object.loadState === 'error' ? 'image-off' : 'image', size: Math.max(16, Math.min(object.totalWidth, object.totalHeight) * 0.3), style: {
27289
+ color: workspace_migrations.KritzelColorHelper.resolveThemeColor({ light: '#9ca3af', dark: '#6b7280' }, currentTheme),
27290
+ } }))), workspace_migrations.KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement') && (index.h("div", { ref: el => el && object.mount(el), style: {
27209
27291
  position: 'absolute',
27210
27292
  left: '0',
27211
27293
  top: '0',
@@ -28829,7 +28911,7 @@ const KritzelPortal = class {
28829
28911
  * This file is auto-generated by the version bump scripts.
28830
28912
  * Do not modify manually.
28831
28913
  */
28832
- const KRITZEL_VERSION = '0.1.92';
28914
+ const KRITZEL_VERSION = '0.1.94';
28833
28915
 
28834
28916
  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)}`;
28835
28917
 
@@ -608,6 +608,20 @@ class KritzelBaseObject {
608
608
  Object.assign(this, object);
609
609
  return this;
610
610
  }
611
+ /**
612
+ * Copies transient (non-persisted) state from a previous local instance
613
+ * onto this freshly-revived instance. Called when a remote Yjs update
614
+ * replaces an existing object so that local-only fields (e.g. asset
615
+ * load state, resolved blob URLs) survive across remote position
616
+ * updates and don't visually flicker.
617
+ *
618
+ * Default implementation is a no-op; subclasses with transient fields
619
+ * should override.
620
+ * @param previous - The previous local instance being replaced.
621
+ */
622
+ adoptTransientStateFrom(_previous) {
623
+ // no-op by default
624
+ }
611
625
  /**
612
626
  * Type guard to check if this object is of a specific class type.
613
627
  * Compares against the __class__ property.
@@ -16241,6 +16255,20 @@ class KritzelImage extends KritzelBaseObject {
16241
16255
  * Not serialized into the Yjs document.
16242
16256
  */
16243
16257
  loadState = 'idle';
16258
+ /**
16259
+ * Maximum number of resolution attempts per page load before the
16260
+ * image is considered permanently failed. Defaults to 3.
16261
+ *
16262
+ * The counter is transient (not persisted to Yjs) and resets every
16263
+ * time a fresh `KritzelImage` instance is constructed, so a full page
16264
+ * reload always grants a fresh budget.
16265
+ */
16266
+ maxLoadAttempts = 3;
16267
+ /**
16268
+ * Number of resolution attempts that have been issued so far for the
16269
+ * current page load. Not serialized into the Yjs document.
16270
+ */
16271
+ loadAttempts = 0;
16244
16272
  /**
16245
16273
  * Creates a new KritzelImage instance.
16246
16274
  * @param config - Optional partial configuration object to initialize image properties
@@ -16260,6 +16288,7 @@ class KritzelImage extends KritzelBaseObject {
16260
16288
  this.height = config?.height || 0;
16261
16289
  this.maxWidth = config?.maxWidth ?? 300;
16262
16290
  this.maxHeight = config?.maxHeight ?? 300;
16291
+ this.maxLoadAttempts = config?.maxLoadAttempts ?? 3;
16263
16292
  }
16264
16293
  /**
16265
16294
  * Factory method to create a new KritzelImage instance with core integration.
@@ -16311,21 +16340,56 @@ class KritzelImage extends KritzelBaseObject {
16311
16340
  // Strip transient, non-persistent rendering state.
16312
16341
  delete plain.resolvedSrc;
16313
16342
  delete plain.loadState;
16343
+ delete plain.loadAttempts;
16314
16344
  return plain;
16315
16345
  }
16346
+ /**
16347
+ * Carries transient asset-loading state (resolved URL, load lifecycle,
16348
+ * retry counters) over from the previous local instance when a remote
16349
+ * Yjs update produces a freshly-revived replacement. Without this,
16350
+ * frequent remote updates (e.g. another user dragging the image)
16351
+ * would reset `loadState` to `'idle'` on every tick and cause the
16352
+ * skeleton to flicker between renders.
16353
+ */
16354
+ adoptTransientStateFrom(previous) {
16355
+ if (!(previous instanceof KritzelImage))
16356
+ return;
16357
+ // Only carry over the resolved URL when the asset reference itself
16358
+ // hasn't changed; otherwise the new asset must be re-resolved.
16359
+ if (previous.assetId === this.assetId) {
16360
+ this.resolvedSrc = previous.resolvedSrc;
16361
+ this.loadState = previous.loadState;
16362
+ this.loadAttempts = previous.loadAttempts;
16363
+ }
16364
+ }
16316
16365
  /**
16317
16366
  * Triggers (idempotent) resolution of the referenced asset. Updates
16318
16367
  * `resolvedSrc` and `loadState` as the resolution progresses and
16319
16368
  * schedules a re-render when the URL is available.
16369
+ *
16370
+ * On failure, retries automatically up to {@link maxLoadAttempts}
16371
+ * times per page load with a short exponential backoff. Once the
16372
+ * budget is exhausted, the image is marked permanently `'error'` for
16373
+ * the remainder of the page load to avoid hammering an unavailable
16374
+ * backend; a full reload (which constructs a fresh instance) grants
16375
+ * a new budget.
16320
16376
  */
16321
16377
  ensureResolved() {
16322
16378
  if (this.loadState === 'loading' || this.loadState === 'ready')
16323
16379
  return;
16380
+ if (this.loadState === 'error')
16381
+ return;
16324
16382
  if (!this.assetId)
16325
16383
  return;
16326
16384
  if (!this._core?.assetResolver)
16327
16385
  return;
16386
+ if (this.loadAttempts >= this.maxLoadAttempts) {
16387
+ this.loadState = 'error';
16388
+ return;
16389
+ }
16328
16390
  this.loadState = 'loading';
16391
+ this.loadAttempts += 1;
16392
+ const attempt = this.loadAttempts;
16329
16393
  this._core.assetResolver
16330
16394
  .resolve(this.assetId)
16331
16395
  .then(url => {
@@ -16334,9 +16398,24 @@ class KritzelImage extends KritzelBaseObject {
16334
16398
  this._core?.rerender();
16335
16399
  })
16336
16400
  .catch(err => {
16337
- this.loadState = 'error';
16338
- console.warn(`[KritzelImage] Failed to resolve asset ${this.assetId}:`, err);
16339
- this._core?.rerender();
16401
+ const attemptsExhausted = this.loadAttempts >= this.maxLoadAttempts;
16402
+ console.warn(`[KritzelImage] Failed to resolve asset ${this.assetId} ` +
16403
+ `(attempt ${attempt}/${this.maxLoadAttempts}):`, err);
16404
+ if (attemptsExhausted) {
16405
+ this.loadState = 'error';
16406
+ this._core?.rerender();
16407
+ return;
16408
+ }
16409
+ // Schedule a retry with a small exponential backoff. Reset to
16410
+ // 'idle' so the next call (or scheduled tick) is allowed to
16411
+ // proceed past the early-return guard.
16412
+ this.loadState = 'idle';
16413
+ const backoffMs = Math.min(2000, 250 * Math.pow(2, attempt - 1));
16414
+ setTimeout(() => {
16415
+ if (this.loadState === 'idle') {
16416
+ this.ensureResolved();
16417
+ }
16418
+ }, backoffMs);
16340
16419
  });
16341
16420
  }
16342
16421
  /**
@@ -16370,13 +16449,15 @@ class KritzelImage extends KritzelBaseObject {
16370
16449
  });
16371
16450
  }
16372
16451
  /**
16373
- * Overrides base mount to kick off asset resolution the first time
16374
- * the image is attached to the DOM. Legacy images persisted with an
16375
- * inline `src` data URL are opportunistically migrated to the asset
16376
- * storage layer on first mount.
16452
+ * Kicks off asset resolution (or legacy data-URL migration) without
16453
+ * requiring the `<img>` element to be mounted. Safe to call multiple
16454
+ * times; both underlying paths are idempotent.
16455
+ *
16456
+ * Useful for the renderer's loading-skeleton path, where the actual
16457
+ * `<img>` is not in the DOM yet but the asset bytes still need to
16458
+ * start loading.
16377
16459
  */
16378
- mount(element) {
16379
- super.mount(element);
16460
+ ensureLoaded() {
16380
16461
  if (this.assetId && !this.resolvedSrc) {
16381
16462
  this.ensureResolved();
16382
16463
  }
@@ -16384,6 +16465,16 @@ class KritzelImage extends KritzelBaseObject {
16384
16465
  this.migrateLegacyDataUrlIfNeeded();
16385
16466
  }
16386
16467
  }
16468
+ /**
16469
+ * Overrides base mount to kick off asset resolution the first time
16470
+ * the image is attached to the DOM. Legacy images persisted with an
16471
+ * inline `src` data URL are opportunistically migrated to the asset
16472
+ * storage layer on first mount.
16473
+ */
16474
+ mount(element) {
16475
+ super.mount(element);
16476
+ this.ensureLoaded();
16477
+ }
16387
16478
  /**
16388
16479
  * Creates a KritzelImage from a URL, handling image loading and dimension calculation.
16389
16480
  * Loads the image, calculates scaled dimensions respecting maxWidth/maxHeight constraints,
@@ -20260,6 +20351,7 @@ KritzelIconRegistry.registerIcons({
20260
20351
  'shape-ellipse': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="12" rx="10" ry="8"/></svg>',
20261
20352
  'shape-triangle': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3L22 21H2L12 3Z"/></svg>',
20262
20353
  'image': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>',
20354
+ 'image-off': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image-off-icon lucide-image-off"><line x1="2" x2="22" y1="2" y2="22"/><path d="M10.41 10.41a2 2 0 1 1-2.83-2.83"/><line x1="13.5" x2="6" y1="13.5" y2="21"/><line x1="18" x2="21" y1="12" y2="15"/><path d="M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59"/><path d="M21 15V5a2 2 0 0 0-2-2H9"/></svg>',
20263
20355
  'chevron-down': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>',
20264
20356
  'chevron-up': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>',
20265
20357
  'chevron-left': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left-icon lucide-chevron-left"><path d="m15 18-6-6 6-6"/></svg>',
@@ -168,12 +168,24 @@ export class KritzelCore {
168
168
  }
169
169
  /**
170
170
  * Initializes the Yjs document for collaborative editing.
171
- * Sets up the app state map with the current sync configuration and
172
- * initializes the asset storage layer.
171
+ * Sets up the app state map with the current sync configuration.
172
+ *
173
+ * Asset storage is initialized separately via {@link initializeAssetStorage}
174
+ * so that it can be deferred until the `assetStorageConfig` prop arrives,
175
+ * mirroring the lazy-init pattern used for sync config.
173
176
  */
174
177
  async initializeYjs() {
175
- await this._assetResolver.init(this._assetStorageConfig);
176
178
  await this._appStateMap.initialize(this, this._syncConfig);
179
+ }
180
+ /**
181
+ * Initializes the asset storage layer with the current asset storage
182
+ * configuration. Safe to call multiple times: subsequent calls are
183
+ * no-ops because {@link KritzelAssetResolver.init} is idempotent.
184
+ * To apply a new configuration after initialization, destroy the
185
+ * resolver first or use {@link KritzelEngine.reinitSync}.
186
+ */
187
+ async initializeAssetStorage() {
188
+ await this._assetResolver.init(this._assetStorageConfig);
177
189
  this.warnIfAssetStorageMismatched();
178
190
  }
179
191
  /**
@@ -371,6 +371,20 @@ export class KritzelBaseObject {
371
371
  Object.assign(this, object);
372
372
  return this;
373
373
  }
374
+ /**
375
+ * Copies transient (non-persisted) state from a previous local instance
376
+ * onto this freshly-revived instance. Called when a remote Yjs update
377
+ * replaces an existing object so that local-only fields (e.g. asset
378
+ * load state, resolved blob URLs) survive across remote position
379
+ * updates and don't visually flicker.
380
+ *
381
+ * Default implementation is a no-op; subclasses with transient fields
382
+ * should override.
383
+ * @param previous - The previous local instance being replaced.
384
+ */
385
+ adoptTransientStateFrom(_previous) {
386
+ // no-op by default
387
+ }
374
388
  /**
375
389
  * Type guard to check if this object is of a specific class type.
376
390
  * Compares against the __class__ property.
@@ -49,6 +49,20 @@ export class KritzelImage extends KritzelBaseObject {
49
49
  * Not serialized into the Yjs document.
50
50
  */
51
51
  loadState = 'idle';
52
+ /**
53
+ * Maximum number of resolution attempts per page load before the
54
+ * image is considered permanently failed. Defaults to 3.
55
+ *
56
+ * The counter is transient (not persisted to Yjs) and resets every
57
+ * time a fresh `KritzelImage` instance is constructed, so a full page
58
+ * reload always grants a fresh budget.
59
+ */
60
+ maxLoadAttempts = 3;
61
+ /**
62
+ * Number of resolution attempts that have been issued so far for the
63
+ * current page load. Not serialized into the Yjs document.
64
+ */
65
+ loadAttempts = 0;
52
66
  /**
53
67
  * Creates a new KritzelImage instance.
54
68
  * @param config - Optional partial configuration object to initialize image properties
@@ -68,6 +82,7 @@ export class KritzelImage extends KritzelBaseObject {
68
82
  this.height = config?.height || 0;
69
83
  this.maxWidth = config?.maxWidth ?? 300;
70
84
  this.maxHeight = config?.maxHeight ?? 300;
85
+ this.maxLoadAttempts = config?.maxLoadAttempts ?? 3;
71
86
  }
72
87
  /**
73
88
  * Factory method to create a new KritzelImage instance with core integration.
@@ -119,21 +134,56 @@ export class KritzelImage extends KritzelBaseObject {
119
134
  // Strip transient, non-persistent rendering state.
120
135
  delete plain.resolvedSrc;
121
136
  delete plain.loadState;
137
+ delete plain.loadAttempts;
122
138
  return plain;
123
139
  }
140
+ /**
141
+ * Carries transient asset-loading state (resolved URL, load lifecycle,
142
+ * retry counters) over from the previous local instance when a remote
143
+ * Yjs update produces a freshly-revived replacement. Without this,
144
+ * frequent remote updates (e.g. another user dragging the image)
145
+ * would reset `loadState` to `'idle'` on every tick and cause the
146
+ * skeleton to flicker between renders.
147
+ */
148
+ adoptTransientStateFrom(previous) {
149
+ if (!(previous instanceof KritzelImage))
150
+ return;
151
+ // Only carry over the resolved URL when the asset reference itself
152
+ // hasn't changed; otherwise the new asset must be re-resolved.
153
+ if (previous.assetId === this.assetId) {
154
+ this.resolvedSrc = previous.resolvedSrc;
155
+ this.loadState = previous.loadState;
156
+ this.loadAttempts = previous.loadAttempts;
157
+ }
158
+ }
124
159
  /**
125
160
  * Triggers (idempotent) resolution of the referenced asset. Updates
126
161
  * `resolvedSrc` and `loadState` as the resolution progresses and
127
162
  * schedules a re-render when the URL is available.
163
+ *
164
+ * On failure, retries automatically up to {@link maxLoadAttempts}
165
+ * times per page load with a short exponential backoff. Once the
166
+ * budget is exhausted, the image is marked permanently `'error'` for
167
+ * the remainder of the page load to avoid hammering an unavailable
168
+ * backend; a full reload (which constructs a fresh instance) grants
169
+ * a new budget.
128
170
  */
129
171
  ensureResolved() {
130
172
  if (this.loadState === 'loading' || this.loadState === 'ready')
131
173
  return;
174
+ if (this.loadState === 'error')
175
+ return;
132
176
  if (!this.assetId)
133
177
  return;
134
178
  if (!this._core?.assetResolver)
135
179
  return;
180
+ if (this.loadAttempts >= this.maxLoadAttempts) {
181
+ this.loadState = 'error';
182
+ return;
183
+ }
136
184
  this.loadState = 'loading';
185
+ this.loadAttempts += 1;
186
+ const attempt = this.loadAttempts;
137
187
  this._core.assetResolver
138
188
  .resolve(this.assetId)
139
189
  .then(url => {
@@ -142,9 +192,24 @@ export class KritzelImage extends KritzelBaseObject {
142
192
  this._core?.rerender();
143
193
  })
144
194
  .catch(err => {
145
- this.loadState = 'error';
146
- console.warn(`[KritzelImage] Failed to resolve asset ${this.assetId}:`, err);
147
- this._core?.rerender();
195
+ const attemptsExhausted = this.loadAttempts >= this.maxLoadAttempts;
196
+ console.warn(`[KritzelImage] Failed to resolve asset ${this.assetId} ` +
197
+ `(attempt ${attempt}/${this.maxLoadAttempts}):`, err);
198
+ if (attemptsExhausted) {
199
+ this.loadState = 'error';
200
+ this._core?.rerender();
201
+ return;
202
+ }
203
+ // Schedule a retry with a small exponential backoff. Reset to
204
+ // 'idle' so the next call (or scheduled tick) is allowed to
205
+ // proceed past the early-return guard.
206
+ this.loadState = 'idle';
207
+ const backoffMs = Math.min(2000, 250 * Math.pow(2, attempt - 1));
208
+ setTimeout(() => {
209
+ if (this.loadState === 'idle') {
210
+ this.ensureResolved();
211
+ }
212
+ }, backoffMs);
148
213
  });
149
214
  }
150
215
  /**
@@ -178,13 +243,15 @@ export class KritzelImage extends KritzelBaseObject {
178
243
  });
179
244
  }
180
245
  /**
181
- * Overrides base mount to kick off asset resolution the first time
182
- * the image is attached to the DOM. Legacy images persisted with an
183
- * inline `src` data URL are opportunistically migrated to the asset
184
- * storage layer on first mount.
246
+ * Kicks off asset resolution (or legacy data-URL migration) without
247
+ * requiring the `<img>` element to be mounted. Safe to call multiple
248
+ * times; both underlying paths are idempotent.
249
+ *
250
+ * Useful for the renderer's loading-skeleton path, where the actual
251
+ * `<img>` is not in the DOM yet but the asset bytes still need to
252
+ * start loading.
185
253
  */
186
- mount(element) {
187
- super.mount(element);
254
+ ensureLoaded() {
188
255
  if (this.assetId && !this.resolvedSrc) {
189
256
  this.ensureResolved();
190
257
  }
@@ -192,6 +259,16 @@ export class KritzelImage extends KritzelBaseObject {
192
259
  this.migrateLegacyDataUrlIfNeeded();
193
260
  }
194
261
  }
262
+ /**
263
+ * Overrides base mount to kick off asset resolution the first time
264
+ * the image is attached to the DOM. Legacy images persisted with an
265
+ * inline `src` data URL are opportunistically migrated to the asset
266
+ * storage layer on first mount.
267
+ */
268
+ mount(element) {
269
+ super.mount(element);
270
+ this.ensureLoaded();
271
+ }
195
272
  /**
196
273
  * Creates a KritzelImage from a URL, handling image loading and dimension calculation.
197
274
  * Loads the image, calculates scaled dimensions respecting maxWidth/maxHeight constraints,
@@ -64,6 +64,7 @@ KritzelIconRegistry.registerIcons({
64
64
  'shape-ellipse': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="12" rx="10" ry="8"/></svg>',
65
65
  'shape-triangle': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3L22 21H2L12 3Z"/></svg>',
66
66
  'image': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>',
67
+ 'image-off': '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-image-off-icon lucide-image-off"><line x1="2" x2="22" y1="2" y2="22"/><path d="M10.41 10.41a2 2 0 1 1-2.83-2.83"/><line x1="13.5" x2="6" y1="13.5" y2="21"/><line x1="18" x2="21" y1="12" y2="15"/><path d="M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59"/><path d="M21 15V5a2 2 0 0 0-2-2H9"/></svg>',
67
68
  'chevron-down': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>',
68
69
  'chevron-up': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>',
69
70
  'chevron-left': '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left-icon lucide-chevron-left"><path d="m15 18-6-6 6-6"/></svg>',
@@ -438,6 +438,10 @@ export class KritzelObjectMap {
438
438
  objectsToUpdate.forEach(object => {
439
439
  const existed = this._idMap.has(object.id);
440
440
  if (existed) {
441
+ const previous = this._idMap.get(object.id);
442
+ if (previous && typeof object.adoptTransientStateFrom === 'function') {
443
+ object.adoptTransientStateFrom(previous);
444
+ }
441
445
  this.quadtree.update(object);
442
446
  this._idMap.set(object.id, object);
443
447
  updatedObjects.push(object);
@@ -453,6 +457,10 @@ export class KritzelObjectMap {
453
457
  selectionGroupsToUpdate.forEach(object => {
454
458
  const existed = this._idMap.has(object.id);
455
459
  if (existed) {
460
+ const previous = this._idMap.get(object.id);
461
+ if (previous && typeof object.adoptTransientStateFrom === 'function') {
462
+ object.adoptTransientStateFrom(previous);
463
+ }
456
464
  this.quadtree.update(object);
457
465
  this._idMap.set(object.id, object);
458
466
  }