kritzel-stencil 0.1.93 → 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 (66) hide show
  1. package/dist/cjs/index.cjs.js +1 -1
  2. package/dist/cjs/kritzel-active-users_42.cjs.entry.js +36 -4
  3. package/dist/cjs/{workspace.migrations-PaftqSjk.js → workspace.migrations-BENHTbRC.js} +101 -9
  4. package/dist/collection/classes/objects/base-object.class.js +14 -0
  5. package/dist/collection/classes/objects/image.class.js +86 -9
  6. package/dist/collection/classes/registries/icon-registry.class.js +1 -0
  7. package/dist/collection/classes/structures/object-map.structure.js +8 -0
  8. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +26 -2
  9. package/dist/collection/constants/version.js +1 -1
  10. package/dist/components/index.js +1 -1
  11. package/dist/components/kritzel-awareness-cursors.js +1 -1
  12. package/dist/components/kritzel-back-to-content.js +1 -1
  13. package/dist/components/kritzel-brush-style.js +1 -1
  14. package/dist/components/kritzel-context-menu.js +1 -1
  15. package/dist/components/kritzel-controls.js +1 -1
  16. package/dist/components/kritzel-editor.js +1 -1
  17. package/dist/components/kritzel-engine.js +1 -1
  18. package/dist/components/kritzel-export.js +1 -1
  19. package/dist/components/kritzel-icon.js +1 -1
  20. package/dist/components/kritzel-login-dialog.js +1 -1
  21. package/dist/components/kritzel-master-detail.js +1 -1
  22. package/dist/components/kritzel-menu-item.js +1 -1
  23. package/dist/components/kritzel-menu.js +1 -1
  24. package/dist/components/kritzel-more-menu.js +1 -1
  25. package/dist/components/kritzel-pill-tabs.js +1 -1
  26. package/dist/components/kritzel-settings.js +1 -1
  27. package/dist/components/kritzel-share-dialog.js +1 -1
  28. package/dist/components/kritzel-split-button.js +1 -1
  29. package/dist/components/kritzel-tool-config.js +1 -1
  30. package/dist/components/kritzel-utility-panel.js +1 -1
  31. package/dist/components/kritzel-workspace-manager.js +1 -1
  32. package/dist/components/p-A7Ult9iv.js +1 -0
  33. package/dist/components/p-B2kHVHa_.js +1 -0
  34. package/dist/components/{p-CRdrQOlL.js → p-BFQVg_eQ.js} +1 -1
  35. package/dist/components/{p-CrCtvLMx.js → p-BoRQF_Zc.js} +1 -1
  36. package/dist/components/{p-DRbR0Li3.js → p-BvgGpgKP.js} +1 -1
  37. package/dist/components/p-Bx8daVwR.js +9 -0
  38. package/dist/components/{p-BMPgR5Bt.js → p-CFzvz-B2.js} +1 -1
  39. package/dist/components/{p-CKdGsPx9.js → p-CK29qhZR.js} +1 -1
  40. package/dist/components/{p-D8fQwcNC.js → p-CU6kJPth.js} +1 -1
  41. package/dist/components/{p-CneqMLGJ.js → p-CY9ooSqo.js} +1 -1
  42. package/dist/components/{p-7yTPTHbQ.js → p-ChQNi67Z.js} +1 -1
  43. package/dist/components/{p-DXdAYm-y.js → p-ChqeIKg_.js} +1 -1
  44. package/dist/components/{p-DPmAV68B.js → p-CoyqJSjT.js} +1 -1
  45. package/dist/components/{p-CowdEK08.js → p-CqYIRmoh.js} +1 -1
  46. package/dist/components/{p-cVQef3Hq.js → p-Cra28iyu.js} +1 -1
  47. package/dist/components/{p-CBq-KE9C.js → p-Czaea0WP.js} +1 -1
  48. package/dist/components/{p-TIoiUjzO.js → p-DACQ8HHJ.js} +1 -1
  49. package/dist/components/{p-DMvIGnOt.js → p-DVEfOb8T.js} +1 -1
  50. package/dist/components/{p-DBZyCAsW.js → p-DkT0CXfN.js} +1 -1
  51. package/dist/components/{p-BTguWTDZ.js → p-mYhFNPgz.js} +1 -1
  52. package/dist/esm/index.js +2 -2
  53. package/dist/esm/kritzel-active-users_42.entry.js +36 -4
  54. package/dist/esm/{workspace.migrations-Cz5xML0x.js → workspace.migrations-CfJnWHNg.js} +101 -9
  55. package/dist/stencil/index.esm.js +1 -1
  56. package/dist/stencil/{p-38f5183b.entry.js → p-5fdd1dea.entry.js} +2 -2
  57. package/dist/stencil/p-CfJnWHNg.js +1 -0
  58. package/dist/stencil/stencil.esm.js +1 -1
  59. package/dist/types/classes/objects/base-object.class.d.ts +12 -0
  60. package/dist/types/classes/objects/image.class.d.ts +40 -0
  61. package/dist/types/constants/version.d.ts +1 -1
  62. package/package.json +1 -1
  63. package/dist/components/p-B6n0TqdD.js +0 -9
  64. package/dist/components/p-DQK_4lkI.js +0 -1
  65. package/dist/components/p-Gm5hSQ-e.js +0 -1
  66. 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
  }
@@ -27237,7 +27245,7 @@ const KritzelEngine = class {
27237
27245
  opacity: object.markedForRemoval ? '0.5' : object.opacity.toString(),
27238
27246
  pointerEvents: object.markedForRemoval ? 'none' : 'auto',
27239
27247
  overflow: 'visible',
27240
- }, 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: {
27241
27249
  position: 'absolute',
27242
27250
  left: '0',
27243
27251
  top: '0',
@@ -27255,7 +27263,31 @@ const KritzelEngine = class {
27255
27263
  overflow: 'visible',
27256
27264
  userSelect: 'none',
27257
27265
  imageRendering: this.core.store.state.isScaling || this.core.store.state.isPanning ? 'pixelated' : 'auto',
27258
- }, 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: {
27259
27291
  position: 'absolute',
27260
27292
  left: '0',
27261
27293
  top: '0',
@@ -28879,7 +28911,7 @@ const KritzelPortal = class {
28879
28911
  * This file is auto-generated by the version bump scripts.
28880
28912
  * Do not modify manually.
28881
28913
  */
28882
- const KRITZEL_VERSION = '0.1.93';
28914
+ const KRITZEL_VERSION = '0.1.94';
28883
28915
 
28884
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)}`;
28885
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>',
@@ -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
  }
@@ -1552,7 +1552,7 @@ export class KritzelEngine {
1552
1552
  opacity: object.markedForRemoval ? '0.5' : object.opacity.toString(),
1553
1553
  pointerEvents: object.markedForRemoval ? 'none' : 'auto',
1554
1554
  overflow: 'visible',
1555
- }, viewBox: object?.viewBox }, (object.hasStartArrow || object.hasEndArrow) && (h("defs", null, object.hasStartArrow && (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" }, h("path", { d: object.getArrowPath(object.arrows?.start?.style), fill: object.getArrowFill('start'), transform: `scale(${object.getArrowSize('start') / 10})` }))), object.hasEndArrow && (h("marker", { id: object.endMarkerId, markerWidth: object.getArrowSize('end'), markerHeight: object.getArrowSize('end'), refX: 0, refY: object.getArrowSize('end') / 2, orient: "auto", markerUnits: "userSpaceOnUse" }, h("path", { d: object.getArrowPath(object.arrows?.end?.style), fill: object.getArrowFill('end'), transform: `scale(${object.getArrowSize('end') / 10})` }))))), h("path", { d: this.core.anchorManager.computeClippedLinePath(object), fill: "none", stroke: "transparent", "stroke-width": Math.max(object?.strokeWidth || 0, 10), "stroke-linecap": "round" }), h("path", { d: this.core.anchorManager.computeClippedLinePath(object), fill: "none", stroke: 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 }))), KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && (h("img", { ref: el => el && object.mount(el), src: object.resolvedSrc || object.src, style: {
1555
+ }, viewBox: object?.viewBox }, (object.hasStartArrow || object.hasEndArrow) && (h("defs", null, object.hasStartArrow && (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" }, h("path", { d: object.getArrowPath(object.arrows?.start?.style), fill: object.getArrowFill('start'), transform: `scale(${object.getArrowSize('start') / 10})` }))), object.hasEndArrow && (h("marker", { id: object.endMarkerId, markerWidth: object.getArrowSize('end'), markerHeight: object.getArrowSize('end'), refX: 0, refY: object.getArrowSize('end') / 2, orient: "auto", markerUnits: "userSpaceOnUse" }, h("path", { d: object.getArrowPath(object.arrows?.end?.style), fill: object.getArrowFill('end'), transform: `scale(${object.getArrowSize('end') / 10})` }))))), h("path", { d: this.core.anchorManager.computeClippedLinePath(object), fill: "none", stroke: "transparent", "stroke-width": Math.max(object?.strokeWidth || 0, 10), "stroke-linecap": "round" }), h("path", { d: this.core.anchorManager.computeClippedLinePath(object), fill: "none", stroke: 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 }))), KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && object.loadState === 'ready' && (h("img", { ref: el => el && object.mount(el), src: object.resolvedSrc || object.src, style: {
1556
1556
  position: 'absolute',
1557
1557
  left: '0',
1558
1558
  top: '0',
@@ -1570,7 +1570,31 @@ export class KritzelEngine {
1570
1570
  overflow: 'visible',
1571
1571
  userSelect: 'none',
1572
1572
  imageRendering: this.core.store.state.isScaling || this.core.store.state.isPanning ? 'pixelated' : 'auto',
1573
- }, draggable: false, onDragStart: e => e.preventDefault() })), KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement') && (h("div", { ref: el => el && object.mount(el), style: {
1573
+ }, draggable: false, onDragStart: e => e.preventDefault() })), KritzelClassHelper.isInstanceOf(object, 'KritzelImage') && object.loadState !== 'ready' && (h("div", { ref: () => object.ensureLoaded(), style: {
1574
+ position: 'absolute',
1575
+ left: '0',
1576
+ top: '0',
1577
+ width: object.totalWidth + 'px',
1578
+ height: object.totalHeight + 'px',
1579
+ transform: object.rotationDegrees !== 0 ? `rotate(${object.rotationDegrees}deg)` : undefined,
1580
+ transformOrigin: object.rotationDegrees !== 0 ? `${object.totalWidth / 2}px ${object.totalHeight / 2}px` : undefined,
1581
+ opacity: object.markedForRemoval ? '0.5' : object.opacity.toString(),
1582
+ pointerEvents: object.markedForRemoval ? 'none' : 'auto',
1583
+ backgroundColor: KritzelColorHelper.resolveThemeColor({ light: '#e5e7eb', dark: '#2a2a2a' }, currentTheme),
1584
+ borderColor: object.loadState === 'error'
1585
+ ? KritzelColorHelper.resolveThemeColor({ light: '#9ca3af', dark: '#6b7280' }, currentTheme)
1586
+ : KritzelColorHelper.resolveThemeColor(object.borderColor, currentTheme),
1587
+ borderWidth: object.loadState === 'error' ? '1px' : object.borderWidth + 'px',
1588
+ borderStyle: 'solid',
1589
+ padding: object.padding + 'px',
1590
+ overflow: 'hidden',
1591
+ userSelect: 'none',
1592
+ display: 'flex',
1593
+ alignItems: 'center',
1594
+ justifyContent: 'center',
1595
+ } }, h("kritzel-icon", { name: object.loadState === 'error' ? 'image-off' : 'image', size: Math.max(16, Math.min(object.totalWidth, object.totalHeight) * 0.3), style: {
1596
+ color: KritzelColorHelper.resolveThemeColor({ light: '#9ca3af', dark: '#6b7280' }, currentTheme),
1597
+ } }))), KritzelClassHelper.isInstanceOf(object, 'KritzelCustomElement') && (h("div", { ref: el => el && object.mount(el), style: {
1574
1598
  position: 'absolute',
1575
1599
  left: '0',
1576
1600
  top: '0',
@@ -3,4 +3,4 @@
3
3
  * This file is auto-generated by the version bump scripts.
4
4
  * Do not modify manually.
5
5
  */
6
- export const KRITZEL_VERSION = '0.1.93';
6
+ export const KRITZEL_VERSION = '0.1.94';